Don't allow multiple file choosers for custom buddy icons.
[pidgin-git.git] / pidgin / gtkconv.c
blob2ebea7fdae41b535a682d712bb4703ee0d51ea30
1 /* pidgin
3 * Pidgin is the legal property of its developers, whose names are too numerous
4 * to list here. Please refer to the COPYRIGHT file distributed with this
5 * source distribution.
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2 of the License, or
10 * (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License
18 * along with this program; if not, write to the Free Software
19 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA
23 #include "internal.h"
24 #include "pidgin.h"
26 #ifndef _WIN32
27 # include <X11/Xlib.h>
28 #endif
30 #include <gdk/gdkkeysyms.h>
32 #include <talkatu.h>
34 #include "account.h"
35 #include "attention.h"
36 #include "action.h"
37 #include "cmds.h"
38 #include "core.h"
39 #include "debug.h"
40 #include "glibcompat.h"
41 #include "idle.h"
42 #include "image-store.h"
43 #include "log.h"
44 #include "notify.h"
45 #include "plugins.h"
46 #include "protocol.h"
47 #include "request.h"
48 #include "smiley-parser.h"
49 #include "util.h"
50 #include "version.h"
52 #include "gtkinternal.h"
53 #include "gtkdnd-hints.h"
54 #include "gtkblist.h"
55 #include "gtkconv.h"
56 #include "gtkconvwin.h"
57 #include "gtkdialogs.h"
58 #include "gtkmenutray.h"
59 #include "gtkpounce.h"
60 #include "gtkprefs.h"
61 #include "gtkprivacy.h"
62 #include "gtkstyle.h"
63 #include "gtkutils.h"
64 #include "pidgingdkpixbuf.h"
65 #include "pidgininvitedialog.h"
66 #include "pidginlog.h"
67 #include "pidginmessage.h"
68 #include "pidginstock.h"
69 #include "pidgintooltip.h"
71 #include "gtknickcolors.h"
73 #define GTK_TOOLTIPS_VAR gtkconv->tooltips
74 #include "gtk3compat.h"
76 #define ADD_MESSAGE_HISTORY_AT_ONCE 100
79 * A GTK+ Instant Message pane.
81 struct _PidginImPane
83 GtkWidget *block;
84 GtkWidget *send_file;
85 GtkWidget *sep1;
86 GtkWidget *sep2;
87 GtkWidget *check;
88 GtkWidget *progress;
89 guint32 typing_timer;
91 /* Buddy icon stuff */
92 GtkWidget *icon_container;
93 GtkWidget *icon;
94 gboolean show_icon;
95 gboolean animate;
96 GdkPixbufAnimation *anim;
97 GdkPixbufAnimationIter *iter;
98 guint32 icon_timer;
102 * GTK+ Chat panes.
104 struct _PidginChatPane
106 GtkWidget *count;
107 GtkWidget *list;
108 GtkWidget *topic_text;
111 #define CLOSE_CONV_TIMEOUT_SECS (10 * 60)
113 #define AUTO_RESPONSE "&lt;AUTO-REPLY&gt; : "
115 typedef enum
117 PIDGIN_CONV_SET_TITLE = 1 << 0,
118 PIDGIN_CONV_BUDDY_ICON = 1 << 1,
119 PIDGIN_CONV_MENU = 1 << 2,
120 PIDGIN_CONV_TAB_ICON = 1 << 3,
121 PIDGIN_CONV_TOPIC = 1 << 4,
122 PIDGIN_CONV_SMILEY_THEME = 1 << 5,
123 PIDGIN_CONV_COLORIZE_TITLE = 1 << 6,
124 PIDGIN_CONV_E2EE = 1 << 7
125 }PidginConvFields;
127 enum {
128 CONV_ICON_COLUMN,
129 CONV_TEXT_COLUMN,
130 CONV_EMBLEM_COLUMN,
131 CONV_PROTOCOL_ICON_COLUMN,
132 CONV_NUM_COLUMNS
133 } PidginInfopaneColumns;
135 #define PIDGIN_CONV_ALL ((1 << 7) - 1)
137 /* XXX: These color defines shouldn't really be here. But the nick-color
138 * generation algorithm uses them, so keeping these around until we fix that. */
139 #define DEFAULT_SEND_COLOR "#204a87"
140 #define DEFAULT_HIGHLIGHT_COLOR "#AF7F00"
142 #define BUDDYICON_SIZE_MIN 32
143 #define BUDDYICON_SIZE_MAX 96
145 #define MIN_LUMINANCE_CONTRAST_RATIO 4.5
147 #define NICK_COLOR_GENERATE_COUNT 220
148 static GArray *generated_nick_colors = NULL;
150 /* These probably won't conflict with any WebKit values. */
151 #define PIDGIN_DRAG_BLIST_NODE (1337)
152 #define PIDGIN_DRAG_IM_CONTACT (31337)
154 static GtkWidget *invite_dialog = NULL;
155 static GtkWidget *warn_close_dialog = NULL;
157 static PidginConvWindow *hidden_convwin = NULL;
158 static GList *window_list = NULL;
160 /* Lists of status icons at all available sizes for use as window icons */
161 static GList *available_list = NULL;
162 static GList *away_list = NULL;
163 static GList *busy_list = NULL;
164 static GList *xa_list = NULL;
165 static GList *offline_list = NULL;
166 static GHashTable *protocol_lists = NULL;
167 static GHashTable *e2ee_stock = NULL;
169 static gboolean update_send_to_selection(PidginConvWindow *win);
170 static void generate_send_to_items(PidginConvWindow *win);
172 /* Prototypes. <-- because Paco-Paco hates this comment. */
173 static gboolean infopane_entry_activate(PidginConversation *gtkconv);
174 static void got_typing_keypress(PidginConversation *gtkconv, gboolean first);
175 static void gray_stuff_out(PidginConversation *gtkconv);
176 static void add_chat_user_common(PurpleChatConversation *chat, PurpleChatUser *cb, const char *old_name);
177 static void pidgin_conv_updated(PurpleConversation *conv, PurpleConversationUpdateType type);
178 static void conv_set_unseen(PurpleConversation *gtkconv, PidginUnseenState state);
179 static void gtkconv_set_unseen(PidginConversation *gtkconv, PidginUnseenState state);
180 static void update_typing_icon(PidginConversation *gtkconv);
181 static void update_typing_message(PidginConversation *gtkconv, const char *message);
182 gboolean pidgin_conv_has_focus(PurpleConversation *conv);
183 static GArray* generate_nick_colors(guint numcolors, GdkRGBA background);
184 gdouble luminance(GdkRGBA color);
185 static gboolean color_is_visible(GdkRGBA foreground, GdkRGBA background, gdouble min_contrast_ratio);
186 static GtkTextTag *get_buddy_tag(PurpleChatConversation *chat, const char *who, PurpleMessageFlags flag, gboolean create);
187 static void pidgin_conv_update_fields(PurpleConversation *conv, PidginConvFields fields);
188 static void focus_out_from_menubar(GtkWidget *wid, PidginConvWindow *win);
189 static void pidgin_conv_tab_pack(PidginConvWindow *win, PidginConversation *gtkconv);
190 static gboolean infopane_press_cb(GtkWidget *widget, GdkEventButton *e, PidginConversation *conv);
191 static void hide_conv(PidginConversation *gtkconv, gboolean closetimer);
193 static void pidgin_conv_set_position_size(PidginConvWindow *win, int x, int y,
194 int width, int height);
195 static gboolean pidgin_conv_xy_to_right_infopane(PidginConvWindow *win, int x, int y);
197 static const GdkRGBA *
198 get_nick_color(PidginConversation *gtkconv, const gchar *name)
200 static GdkRGBA col;
202 if (name == NULL) {
203 col.red = col.green = col.blue = 0;
204 col.alpha = 1;
205 return &col;
208 col = g_array_index(gtkconv->nick_colors, GdkRGBA,
209 g_str_hash(name) % gtkconv->nick_colors->len);
211 return &col;
214 static PurpleBlistNode *
215 get_conversation_blist_node(PurpleConversation *conv)
217 PurpleAccount *account = purple_conversation_get_account(conv);
218 PurpleBlistNode *node = NULL;
220 if (PURPLE_IS_IM_CONVERSATION(conv)) {
221 node = PURPLE_BLIST_NODE(purple_blist_find_buddy(account, purple_conversation_get_name(conv)));
222 node = node ? node->parent : NULL;
223 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
224 node = PURPLE_BLIST_NODE(purple_blist_find_chat(account, purple_conversation_get_name(conv)));
227 return node;
230 /**************************************************************************
231 * Callbacks
232 **************************************************************************/
234 static gboolean
235 close_this_sucker(gpointer data)
237 PidginConversation *gtkconv = data;
238 GList *list = g_list_copy(gtkconv->convs);
239 g_list_foreach(list, (GFunc)g_object_unref, NULL);
240 g_list_free(list);
241 return FALSE;
244 static gboolean
245 close_conv_cb(GtkButton *button, PidginConversation *gtkconv)
247 /* We are going to destroy the conversations immediately only if the 'close immediately'
248 * preference is selected. Otherwise, close the conversation after a reasonable timeout
249 * (I am going to consider 10 minutes as a 'reasonable timeout' here.
250 * For chats, close immediately if the chat is not in the buddylist, or if the chat is
251 * not marked 'Persistent' */
252 PurpleConversation *conv = gtkconv->active_conv;
253 PurpleAccount *account = purple_conversation_get_account(conv);
254 const char *name = purple_conversation_get_name(conv);
256 if (PURPLE_IS_IM_CONVERSATION(conv)) {
257 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/im/close_immediately"))
258 close_this_sucker(gtkconv);
259 else
260 hide_conv(gtkconv, TRUE);
261 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
262 PurpleChat *chat = purple_blist_find_chat(account, name);
263 if (!chat ||
264 !purple_blist_node_get_bool(&chat->node, "gtk-persistent"))
265 close_this_sucker(gtkconv);
266 else
267 hide_conv(gtkconv, FALSE);
270 return TRUE;
273 static gboolean
274 lbox_size_allocate_cb(GtkWidget *w, GtkAllocation *allocation, gpointer data)
276 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/chat/userlist_width", allocation->width == 1 ? 0 : allocation->width);
278 return FALSE;
281 static const char *
282 pidgin_get_cmd_prefix(void)
284 return "/";
287 static PurpleCmdRet
288 say_command_cb(PurpleConversation *conv,
289 const char *cmd, char **args, char **error, void *data)
291 purple_conversation_send(conv, args[0]);
293 return PURPLE_CMD_RET_OK;
296 static PurpleCmdRet
297 me_command_cb(PurpleConversation *conv,
298 const char *cmd, char **args, char **error, void *data)
300 char *tmp;
302 tmp = g_strdup_printf("/me %s", args[0]);
303 purple_conversation_send(conv, tmp);
305 g_free(tmp);
306 return PURPLE_CMD_RET_OK;
309 static PurpleCmdRet
310 debug_command_cb(PurpleConversation *conv,
311 const char *cmd, char **args, char **error, void *data)
313 char *tmp, *markup;
315 if (!g_ascii_strcasecmp(args[0], "version")) {
316 tmp = g_strdup_printf("Using Pidgin v%s with libpurple v%s.",
317 DISPLAY_VERSION, purple_core_get_version());
318 } else if (!g_ascii_strcasecmp(args[0], "plugins")) {
319 /* Show all the loaded plugins, including plugins marked internal.
320 * This is intentional, since third party protocols are often sources of bugs, and some
321 * plugin loaders can also be buggy.
323 GString *str = g_string_new("Loaded Plugins: ");
324 const GList *plugins = purple_plugins_get_loaded();
325 if (plugins) {
326 for (; plugins; plugins = plugins->next) {
327 GPluginPluginInfo *info = GPLUGIN_PLUGIN_INFO(
328 purple_plugin_get_info(
329 PURPLE_PLUGIN(plugins->data)));
330 str = g_string_append(
331 str,
332 gplugin_plugin_info_get_name(info));
334 if (plugins->next)
335 str = g_string_append(str, ", ");
337 } else {
338 str = g_string_append(str, "(none)");
341 tmp = g_string_free(str, FALSE);
342 } else if (!g_ascii_strcasecmp(args[0], "unsafe")) {
343 if (purple_debug_is_unsafe()) {
344 purple_debug_set_unsafe(FALSE);
345 purple_conversation_write_system_message(conv,
346 _("Unsafe debugging is now disabled."),
347 PURPLE_MESSAGE_NO_LOG);
348 } else {
349 purple_debug_set_unsafe(TRUE);
350 purple_conversation_write_system_message(conv,
351 _("Unsafe debugging is now enabled."),
352 PURPLE_MESSAGE_NO_LOG);
355 return PURPLE_CMD_RET_OK;
356 } else if (!g_ascii_strcasecmp(args[0], "verbose")) {
357 if (purple_debug_is_verbose()) {
358 purple_debug_set_verbose(FALSE);
359 purple_conversation_write_system_message(conv,
360 _("Verbose debugging is now disabled."),
361 PURPLE_MESSAGE_NO_LOG);
362 } else {
363 purple_debug_set_verbose(TRUE);
364 purple_conversation_write_system_message(conv,
365 _("Verbose debugging is now enabled."),
366 PURPLE_MESSAGE_NO_LOG);
369 return PURPLE_CMD_RET_OK;
370 } else {
371 purple_conversation_write_system_message(conv,
372 _("Supported debug options are: plugins, version, unsafe, verbose"),
373 PURPLE_MESSAGE_NO_LOG);
374 return PURPLE_CMD_RET_OK;
377 markup = g_markup_escape_text(tmp, -1);
378 purple_conversation_send(conv, markup);
380 g_free(tmp);
381 g_free(markup);
382 return PURPLE_CMD_RET_OK;
385 static void clear_conversation_scrollback_cb(PurpleConversation *conv,
386 void *data)
388 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
390 if (PIDGIN_CONVERSATION(conv)) {
391 gtkconv->last_flags = 0;
395 static PurpleCmdRet
396 clear_command_cb(PurpleConversation *conv,
397 const char *cmd, char **args, char **error, void *data)
399 purple_conversation_clear_message_history(conv);
400 return PURPLE_CMD_RET_OK;
403 static PurpleCmdRet
404 clearall_command_cb(PurpleConversation *conv,
405 const char *cmd, char **args, char **error, void *data)
407 GList *l;
408 for (l = purple_conversations_get_all(); l != NULL; l = l->next)
409 purple_conversation_clear_message_history(PURPLE_CONVERSATION(l->data));
411 return PURPLE_CMD_RET_OK;
414 static PurpleCmdRet
415 help_command_cb(PurpleConversation *conv,
416 const char *cmd, char **args, char **error, void *data)
418 GList *l, *text;
419 GString *s;
421 if (args[0] != NULL) {
422 s = g_string_new("");
423 text = purple_cmd_help(conv, args[0]);
425 if (text) {
426 for (l = text; l; l = l->next)
427 if (l->next)
428 g_string_append_printf(s, "%s\n", (char *)l->data);
429 else
430 g_string_append_printf(s, "%s", (char *)l->data);
431 } else {
432 g_string_append(s, _("No such command (in this context)."));
434 } else {
435 s = g_string_new(_("Use \"/help &lt;command&gt;\" for help with a "
436 "specific command.<br/>The following commands are available "
437 "in this context:<br/>"));
439 text = purple_cmd_list(conv);
440 for (l = text; l; l = l->next)
441 if (l->next)
442 g_string_append_printf(s, "%s, ", (char *)l->data);
443 else
444 g_string_append_printf(s, "%s.", (char *)l->data);
445 g_list_free(text);
448 purple_conversation_write_system_message(conv, s->str, PURPLE_MESSAGE_NO_LOG);
449 g_string_free(s, TRUE);
451 return PURPLE_CMD_RET_OK;
454 static void
455 send_history_add(PidginConversation *gtkconv, const char *message)
457 GList *first;
459 first = g_list_first(gtkconv->send_history);
460 g_free(first->data);
461 first->data = g_strdup(message);
462 gtkconv->send_history = g_list_prepend(first, NULL);
465 static gboolean
466 check_for_and_do_command(PurpleConversation *conv)
468 PidginConversation *gtkconv;
469 GtkWidget *view = NULL;
470 GtkTextBuffer *buffer = NULL;
471 gchar *cmd;
472 const gchar *prefix;
473 gboolean retval = FALSE;
475 gtkconv = PIDGIN_CONVERSATION(conv);
476 prefix = pidgin_get_cmd_prefix();
478 view = talkatu_editor_get_view(TALKATU_EDITOR(gtkconv->editor));
479 buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(view));
481 cmd = talkatu_buffer_get_plain_text(TALKATU_BUFFER(buffer));
483 if (cmd && purple_str_has_prefix(cmd, prefix)) {
484 PurpleCmdStatus status;
485 char *error, *cmdline, *markup, *send_history;
487 send_history = talkatu_markup_get_html(buffer, NULL);
488 send_history_add(gtkconv, send_history);
489 g_free(send_history);
491 cmdline = cmd + strlen(prefix);
493 if (purple_strequal(cmdline, "xyzzy")) {
494 purple_conversation_write_system_message(conv,
495 "Nothing happens", PURPLE_MESSAGE_NO_LOG);
496 g_free(cmd);
497 return TRUE;
500 /* Docs are unclear on whether or not prefix should be removed from
501 * the markup so, ignoring for now. Notably if the markup is
502 * `<b>/foo arg1</b>` we now have to move the bold tag around?
503 * - gk 20190709 */
504 markup = talkatu_markup_get_html(buffer, NULL);
505 status = purple_cmd_do_command(conv, cmdline, markup, &error);
506 g_free(markup);
508 switch (status) {
509 case PURPLE_CMD_STATUS_OK:
510 retval = TRUE;
511 break;
512 case PURPLE_CMD_STATUS_NOT_FOUND:
514 PurpleProtocol *protocol = NULL;
515 PurpleConnection *gc;
517 if ((gc = purple_conversation_get_connection(conv)))
518 protocol = purple_connection_get_protocol(gc);
520 if ((protocol != NULL) && (purple_protocol_get_options(protocol) & OPT_PROTO_SLASH_COMMANDS_NATIVE)) {
521 char *spaceslash;
523 /* If the first word in the entered text has a '/' in it, then the user
524 * probably didn't mean it as a command. So send the text as message. */
525 spaceslash = cmdline;
526 while (*spaceslash && *spaceslash != ' ' && *spaceslash != '/')
527 spaceslash++;
529 if (*spaceslash != '/') {
530 purple_conversation_write_system_message(conv,
531 _("Unknown command."), PURPLE_MESSAGE_NO_LOG);
532 retval = TRUE;
535 break;
537 case PURPLE_CMD_STATUS_WRONG_ARGS:
538 purple_conversation_write_system_message(conv,
539 _("Syntax Error: You typed the wrong "
540 "number of arguments to that command."),
541 PURPLE_MESSAGE_NO_LOG);
542 retval = TRUE;
543 break;
544 case PURPLE_CMD_STATUS_FAILED:
545 purple_conversation_write_system_message(conv,
546 error ? error : _("Your command failed for an unknown reason."),
547 PURPLE_MESSAGE_NO_LOG);
548 g_free(error);
549 retval = TRUE;
550 break;
551 case PURPLE_CMD_STATUS_WRONG_TYPE:
552 if(PURPLE_IS_IM_CONVERSATION(conv))
553 purple_conversation_write_system_message(conv,
554 _("That command only works in chats, not IMs."),
555 PURPLE_MESSAGE_NO_LOG);
556 else
557 purple_conversation_write_system_message(conv,
558 _("That command only works in IMs, not chats."),
559 PURPLE_MESSAGE_NO_LOG);
560 retval = TRUE;
561 break;
562 case PURPLE_CMD_STATUS_WRONG_PROTOCOL:
563 purple_conversation_write_system_message(conv,
564 _("That command doesn't work on this protocol."),
565 PURPLE_MESSAGE_NO_LOG);
566 retval = TRUE;
567 break;
571 g_free(cmd);
573 return retval;
576 static void
577 send_cb(GtkWidget *widget, PidginConversation *gtkconv)
579 PurpleConversation *conv = gtkconv->active_conv;
580 PurpleAccount *account;
581 PurpleMessageFlags flags = 0;
582 GtkTextBuffer *buffer = NULL;
583 gchar *content;
585 account = purple_conversation_get_account(conv);
587 buffer = talkatu_editor_get_buffer(TALKATU_EDITOR(gtkconv->editor));
589 if (check_for_and_do_command(conv)) {
590 talkatu_buffer_clear(TALKATU_BUFFER(buffer));
591 return;
594 if (PURPLE_IS_CHAT_CONVERSATION(conv) &&
595 purple_chat_conversation_has_left(PURPLE_CHAT_CONVERSATION(conv))) {
596 return;
599 if (!purple_account_is_connected(account)) {
600 return;
603 content = talkatu_markup_get_html(buffer, NULL);
604 if (purple_strequal(content, "")) {
605 g_free(content);
606 return;
609 purple_idle_touch();
611 /* XXX: is there a better way to tell if the message has images? */
612 // if (strstr(buf, "<img ") != NULL)
613 // flags |= PURPLE_MESSAGE_IMAGES;
615 purple_conversation_send_with_flags(conv, content, flags);
617 g_free(content);
619 talkatu_buffer_clear(TALKATU_BUFFER(buffer));
620 gtkconv_set_unseen(gtkconv, PIDGIN_UNSEEN_NONE);
623 static void
624 add_remove_cb(GtkWidget *widget, PidginConversation *gtkconv)
626 PurpleAccount *account;
627 const char *name;
628 PurpleConversation *conv = gtkconv->active_conv;
630 account = purple_conversation_get_account(conv);
631 name = purple_conversation_get_name(conv);
633 if (PURPLE_IS_IM_CONVERSATION(conv)) {
634 PurpleBuddy *b;
636 b = purple_blist_find_buddy(account, name);
637 if (b != NULL)
638 pidgin_dialogs_remove_buddy(b);
639 else if (account != NULL && purple_account_is_connected(account))
640 purple_blist_request_add_buddy(account, (char *)name, NULL, NULL);
641 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
642 PurpleChat *c;
644 c = purple_blist_find_chat(account, name);
645 if (c != NULL)
646 pidgin_dialogs_remove_chat(c);
647 else if (account != NULL && purple_account_is_connected(account))
648 purple_blist_request_add_chat(account, NULL, NULL, name);
652 static void chat_do_info(PidginConversation *gtkconv, const char *who)
654 PurpleChatConversation *chat = PURPLE_CHAT_CONVERSATION(gtkconv->active_conv);
655 PurpleConnection *gc;
657 if ((gc = purple_conversation_get_connection(gtkconv->active_conv))) {
658 pidgin_retrieve_user_info_in_chat(gc, who, purple_chat_conversation_get_id(chat));
663 static void
664 info_cb(GtkWidget *widget, PidginConversation *gtkconv)
666 PurpleConversation *conv = gtkconv->active_conv;
668 if (PURPLE_IS_IM_CONVERSATION(conv)) {
669 pidgin_retrieve_user_info(purple_conversation_get_connection(conv),
670 purple_conversation_get_name(conv));
671 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
672 /* Get info of the person currently selected in the GtkTreeView */
673 PidginChatPane *gtkchat;
674 GtkTreeIter iter;
675 GtkTreeModel *model;
676 GtkTreeSelection *sel;
677 char *name;
679 gtkchat = gtkconv->u.chat;
681 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
682 sel = gtk_tree_view_get_selection(GTK_TREE_VIEW(gtkchat->list));
684 if (gtk_tree_selection_get_selected(sel, NULL, &iter))
685 gtk_tree_model_get(GTK_TREE_MODEL(model), &iter, CHAT_USERS_NAME_COLUMN, &name, -1);
686 else
687 return;
689 chat_do_info(gtkconv, name);
690 g_free(name);
694 static void
695 block_cb(GtkWidget *widget, PidginConversation *gtkconv)
697 PurpleConversation *conv = gtkconv->active_conv;
698 PurpleAccount *account;
700 account = purple_conversation_get_account(conv);
702 if (account != NULL && purple_account_is_connected(account))
703 pidgin_request_add_block(account, purple_conversation_get_name(conv));
706 static void
707 unblock_cb(GtkWidget *widget, PidginConversation *gtkconv)
709 PurpleConversation *conv = gtkconv->active_conv;
710 PurpleAccount *account;
712 account = purple_conversation_get_account(conv);
714 if (account != NULL && purple_account_is_connected(account))
715 pidgin_request_add_permit(account, purple_conversation_get_name(conv));
718 static void
719 do_invite(GtkWidget *w, int resp, gpointer data)
721 PidginInviteDialog *dialog = PIDGIN_INVITE_DIALOG(w);
722 PurpleChatConversation *chat = pidgin_invite_dialog_get_conversation(dialog);
723 const gchar *contact, *message;
725 if (resp == GTK_RESPONSE_ACCEPT) {
726 contact = pidgin_invite_dialog_get_contact(dialog);
727 if (!g_ascii_strcasecmp(contact, ""))
728 return;
730 message = pidgin_invite_dialog_get_message(dialog);
732 purple_serv_chat_invite(purple_conversation_get_connection(PURPLE_CONVERSATION(chat)),
733 purple_chat_conversation_get_id(chat),
734 message, contact);
737 g_clear_pointer(&invite_dialog, gtk_widget_destroy);
740 static void
741 invite_cb(GtkWidget *widget, PidginConversation *gtkconv)
743 PurpleChatConversation *chat = PURPLE_CHAT_CONVERSATION(gtkconv->active_conv);
745 if (invite_dialog == NULL) {
746 invite_dialog = pidgin_invite_dialog_new(chat);
748 /* Connect the signals. */
749 g_signal_connect(G_OBJECT(invite_dialog), "response",
750 G_CALLBACK(do_invite), NULL);
753 gtk_widget_show_all(invite_dialog);
756 static void
757 menu_new_conv_cb(GtkAction *action, gpointer data)
759 pidgin_dialogs_im();
762 static void
763 menu_join_chat_cb(GtkAction *action, gpointer data)
765 pidgin_blist_joinchat_show();
768 static void
769 savelog_writefile_cb(void *user_data, const char *filename)
771 PurpleConversation *conv = (PurpleConversation *)user_data;
772 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
773 GtkTextBuffer *buffer = NULL;
774 FILE *fp;
775 const char *name;
776 gchar *text;
778 if ((fp = g_fopen(filename, "w+")) == NULL) {
779 purple_notify_error(PIDGIN_CONVERSATION(conv), NULL,
780 _("Unable to open file."), NULL,
781 purple_request_cpar_from_conversation(conv));
782 return;
785 name = purple_conversation_get_name(conv);
787 fprintf(fp, "<html>\n");
788 fprintf(fp, "<head>\n");
789 fprintf(fp, "<meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\">\n");
790 fprintf(fp, "<title>%s</title>\n", name);
791 fprintf(fp, "</head>\n");
793 fprintf(fp, "<body>\n");
794 fprintf(fp, _("<h1>Conversation with %s</h1>\n"), name);
795 buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(gtkconv->history));
796 text = talkatu_markup_get_html(buffer, NULL);
797 fprintf(fp, "%s", text);
798 g_free(text);
799 fprintf(fp, "\n</body>\n");
801 fprintf(fp, "</html>\n");
802 fclose(fp);
806 * It would be kinda cool if this gave the option of saving a
807 * plaintext v. HTML file.
809 static void
810 menu_save_as_cb(GtkAction *action, gpointer data)
812 PidginConvWindow *win = data;
813 PurpleConversation *conv = pidgin_conv_window_get_active_conversation(win);
814 PurpleAccount *account = purple_conversation_get_account(conv);
815 PurpleBuddy *buddy = purple_blist_find_buddy(account, purple_conversation_get_name(conv));
816 const char *name;
817 gchar *buf;
818 gchar *c;
820 if (buddy != NULL)
821 name = purple_buddy_get_contact_alias(buddy);
822 else
823 name = purple_normalize(account, purple_conversation_get_name(conv));
825 buf = g_strdup_printf("%s.html", name);
826 for (c = buf ; *c ; c++)
828 if (*c == '/' || *c == '\\')
829 *c = ' ';
831 purple_request_file(PIDGIN_CONVERSATION(conv), _("Save Conversation"),
832 buf, TRUE, G_CALLBACK(savelog_writefile_cb), NULL,
833 purple_request_cpar_from_conversation(conv), conv);
835 g_free(buf);
838 static void
839 menu_view_log_cb(GtkAction *action, gpointer data)
841 PidginConvWindow *win = data;
842 PurpleConversation *conv;
843 PurpleLogType type;
844 PidginBuddyList *gtkblist;
845 const char *name;
846 PurpleAccount *account;
847 GSList *buddies;
848 GSList *cur;
850 conv = pidgin_conv_window_get_active_conversation(win);
852 if (PURPLE_IS_IM_CONVERSATION(conv))
853 type = PURPLE_LOG_IM;
854 else if (PURPLE_IS_CHAT_CONVERSATION(conv))
855 type = PURPLE_LOG_CHAT;
856 else
857 return;
859 gtkblist = pidgin_blist_get_default_gtk_blist();
861 pidgin_set_cursor(gtkblist->window, GDK_WATCH);
862 pidgin_set_cursor(win->window, GDK_WATCH);
864 name = purple_conversation_get_name(conv);
865 account = purple_conversation_get_account(conv);
867 buddies = purple_blist_find_buddies(account, name);
868 for (cur = buddies; cur != NULL; cur = cur->next)
870 PurpleBlistNode *node = cur->data;
871 if ((node != NULL) && ((node->prev != NULL) || (node->next != NULL)))
873 pidgin_log_show_contact((PurpleContact *)node->parent);
874 g_slist_free(buddies);
875 pidgin_clear_cursor(gtkblist->window);
876 pidgin_clear_cursor(win->window);
877 return;
880 g_slist_free(buddies);
882 pidgin_log_show(type, name, account);
884 pidgin_clear_cursor(gtkblist->window);
885 pidgin_clear_cursor(win->window);
888 static void
889 menu_clear_cb(GtkAction *action, gpointer data)
891 PidginConvWindow *win = data;
892 PurpleConversation *conv;
894 conv = pidgin_conv_window_get_active_conversation(win);
895 purple_conversation_clear_message_history(conv);
898 static void
899 menu_find_cb(GtkAction *action, gpointer data)
901 PidginConvWindow *gtkwin = data;
902 PidginConversation *gtkconv = pidgin_conv_window_get_active_gtkconv(gtkwin);
903 gtk_widget_show_all(gtkconv->quickfind_container);
904 gtk_widget_grab_focus(gtkconv->quickfind_entry);
907 #ifdef USE_VV
908 static void
909 menu_initiate_media_call_cb(GtkAction *action, gpointer data)
911 PidginConvWindow *win = (PidginConvWindow *)data;
912 PurpleConversation *conv = pidgin_conv_window_get_active_conversation(win);
913 PurpleAccount *account = purple_conversation_get_account(conv);
915 purple_protocol_initiate_media(account,
916 purple_conversation_get_name(conv),
917 action == win->menu->audio_call ? PURPLE_MEDIA_AUDIO :
918 action == win->menu->video_call ? PURPLE_MEDIA_VIDEO :
919 action == win->menu->audio_video_call ? PURPLE_MEDIA_AUDIO |
920 PURPLE_MEDIA_VIDEO : PURPLE_MEDIA_NONE);
922 #endif
924 static void
925 menu_send_file_cb(GtkAction *action, gpointer data)
927 PidginConvWindow *win = data;
928 PurpleConversation *conv = pidgin_conv_window_get_active_conversation(win);
930 if (PURPLE_IS_IM_CONVERSATION(conv)) {
931 purple_serv_send_file(purple_conversation_get_connection(conv), purple_conversation_get_name(conv), NULL);
936 static void
937 menu_get_attention_cb(GObject *obj, gpointer data)
939 PidginConvWindow *win = data;
940 PurpleConversation *conv = pidgin_conv_window_get_active_conversation(win);
942 if (PURPLE_IS_IM_CONVERSATION(conv)) {
943 int index;
944 if ((GtkAction *)obj == win->menu->get_attention)
945 index = 0;
946 else
947 index = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(obj), "index"));
948 purple_protocol_send_attention(purple_conversation_get_connection(conv),
949 purple_conversation_get_name(conv), index);
953 static void
954 menu_add_pounce_cb(GtkAction *action, gpointer data)
956 PidginConvWindow *win = data;
957 PurpleConversation *conv;
959 conv = pidgin_conv_window_get_active_gtkconv(win)->active_conv;
961 pidgin_pounce_editor_show(purple_conversation_get_account(conv),
962 purple_conversation_get_name(conv), NULL);
965 static void
966 menu_alias_cb(GtkAction *action, gpointer data)
968 PidginConvWindow *win = data;
969 PurpleConversation *conv;
970 PurpleAccount *account;
971 const char *name;
973 conv = pidgin_conv_window_get_active_conversation(win);
974 account = purple_conversation_get_account(conv);
975 name = purple_conversation_get_name(conv);
977 if (PURPLE_IS_IM_CONVERSATION(conv)) {
978 PurpleBuddy *b;
980 b = purple_blist_find_buddy(account, name);
981 if (b != NULL)
982 pidgin_dialogs_alias_buddy(b);
983 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
984 PurpleChat *c;
986 c = purple_blist_find_chat(account, name);
987 if (c != NULL)
988 pidgin_dialogs_alias_chat(c);
992 static void
993 menu_get_info_cb(GtkAction *action, gpointer data)
995 PidginConvWindow *win = data;
996 PurpleConversation *conv;
998 conv = pidgin_conv_window_get_active_conversation(win);
1000 info_cb(NULL, PIDGIN_CONVERSATION(conv));
1003 static void
1004 menu_invite_cb(GtkAction *action, gpointer data)
1006 PidginConvWindow *win = data;
1007 PurpleConversation *conv;
1009 conv = pidgin_conv_window_get_active_conversation(win);
1011 invite_cb(NULL, PIDGIN_CONVERSATION(conv));
1014 static void
1015 menu_block_cb(GtkAction *action, gpointer data)
1017 PidginConvWindow *win = data;
1018 PurpleConversation *conv;
1020 conv = pidgin_conv_window_get_active_conversation(win);
1022 block_cb(NULL, PIDGIN_CONVERSATION(conv));
1025 static void
1026 menu_unblock_cb(GtkAction *action, gpointer data)
1028 PidginConvWindow *win = data;
1029 PurpleConversation *conv;
1031 conv = pidgin_conv_window_get_active_conversation(win);
1033 unblock_cb(NULL, PIDGIN_CONVERSATION(conv));
1036 static void
1037 menu_add_remove_cb(GtkAction *action, gpointer data)
1039 PidginConvWindow *win = data;
1040 PurpleConversation *conv;
1042 conv = pidgin_conv_window_get_active_conversation(win);
1044 add_remove_cb(NULL, PIDGIN_CONVERSATION(conv));
1047 static gboolean
1048 close_already(gpointer data)
1050 g_object_unref(data);
1051 return FALSE;
1054 static void
1055 hide_conv(PidginConversation *gtkconv, gboolean closetimer)
1057 GList *list;
1059 purple_signal_emit(pidgin_conversations_get_handle(),
1060 "conversation-hiding", gtkconv);
1062 for (list = g_list_copy(gtkconv->convs); list; list = g_list_delete_link(list, list)) {
1063 PurpleConversation *conv = list->data;
1064 if (closetimer) {
1065 guint timer = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(conv), "close-timer"));
1066 if (timer)
1067 g_source_remove(timer);
1068 timer = g_timeout_add_seconds(CLOSE_CONV_TIMEOUT_SECS, close_already, conv);
1069 g_object_set_data(G_OBJECT(conv), "close-timer", GINT_TO_POINTER(timer));
1071 pidgin_conv_window_remove_gtkconv(gtkconv->win, gtkconv);
1072 pidgin_conv_window_add_gtkconv(hidden_convwin, gtkconv);
1076 static void
1077 menu_close_conv_cb(GtkAction *action, gpointer data)
1079 PidginConvWindow *win = data;
1081 close_conv_cb(NULL, PIDGIN_CONVERSATION(pidgin_conv_window_get_active_conversation(win)));
1084 static void
1085 menu_logging_cb(GtkAction *action, gpointer data)
1087 PidginConvWindow *win = data;
1088 PurpleConversation *conv;
1089 gboolean logging;
1090 PurpleBlistNode *node;
1092 conv = pidgin_conv_window_get_active_conversation(win);
1094 if (conv == NULL)
1095 return;
1097 logging = gtk_toggle_action_get_active(GTK_TOGGLE_ACTION(action));
1099 if (logging == purple_conversation_is_logging(conv))
1100 return;
1102 node = get_conversation_blist_node(conv);
1104 if (logging)
1106 /* Enable logging first so the message below can be logged. */
1107 purple_conversation_set_logging(conv, TRUE);
1109 purple_conversation_write_system_message(conv,
1110 _("Logging started. Future messages in this conversation will be logged."), 0);
1112 else
1114 purple_conversation_write_system_message(conv,
1115 _("Logging stopped. Future messages in this conversation will not be logged."), 0);
1117 /* Disable the logging second, so that the above message can be logged. */
1118 purple_conversation_set_logging(conv, FALSE);
1121 /* Save the setting IFF it's different than the pref. */
1122 if (PURPLE_IS_IM_CONVERSATION(conv)) {
1123 if (logging == purple_prefs_get_bool("/purple/logging/log_ims"))
1124 purple_blist_node_remove_setting(node, "enable-logging");
1125 else
1126 purple_blist_node_set_bool(node, "enable-logging", logging);
1127 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
1128 if (logging == purple_prefs_get_bool("/purple/logging/log_chats"))
1129 purple_blist_node_remove_setting(node, "enable-logging");
1130 else
1131 purple_blist_node_set_bool(node, "enable-logging", logging);
1135 static void
1136 menu_toolbar_cb(GtkAction *action, gpointer data)
1138 purple_prefs_set_bool(PIDGIN_PREFS_ROOT "/conversations/show_formatting_toolbar",
1139 gtk_toggle_action_get_active(GTK_TOGGLE_ACTION(action)));
1142 static void
1143 menu_sounds_cb(GtkAction *action, gpointer data)
1145 PidginConvWindow *win = data;
1146 PurpleConversation *conv;
1147 PidginConversation *gtkconv;
1148 PurpleBlistNode *node;
1150 conv = pidgin_conv_window_get_active_conversation(win);
1152 if (!conv)
1153 return;
1155 gtkconv = PIDGIN_CONVERSATION(conv);
1157 gtkconv->make_sound =
1158 gtk_toggle_action_get_active(GTK_TOGGLE_ACTION(action));
1159 node = get_conversation_blist_node(conv);
1160 if (node)
1161 purple_blist_node_set_bool(node, "gtk-mute-sound", !gtkconv->make_sound);
1164 static void
1165 chat_do_im(PidginConversation *gtkconv, const char *who)
1167 PurpleConversation *conv = gtkconv->active_conv;
1168 PurpleAccount *account;
1169 PurpleConnection *gc;
1170 PurpleProtocol *protocol = NULL;
1171 gchar *real_who = NULL;
1173 account = purple_conversation_get_account(conv);
1174 g_return_if_fail(account != NULL);
1176 gc = purple_account_get_connection(account);
1177 g_return_if_fail(gc != NULL);
1179 protocol = purple_connection_get_protocol(gc);
1181 if (protocol)
1182 real_who = purple_protocol_chat_iface_get_user_real_name(protocol, gc,
1183 purple_chat_conversation_get_id(PURPLE_CHAT_CONVERSATION(conv)), who);
1185 if(!who && !real_who)
1186 return;
1188 pidgin_dialogs_im_with_user(account, real_who ? real_who : who);
1190 g_free(real_who);
1193 static void pidgin_conv_chat_update_user(PurpleChatUser *chatuser);
1195 static void
1196 ignore_cb(GtkWidget *w, PidginConversation *gtkconv)
1198 PurpleChatConversation *chat = PURPLE_CHAT_CONVERSATION(gtkconv->active_conv);
1199 const char *name;
1201 name = g_object_get_data(G_OBJECT(w), "user_data");
1203 if (name == NULL)
1204 return;
1206 if (purple_chat_conversation_is_ignored_user(chat, name))
1207 purple_chat_conversation_unignore(chat, name);
1208 else
1209 purple_chat_conversation_ignore(chat, name);
1211 pidgin_conv_chat_update_user(purple_chat_conversation_find_user(chat, name));
1214 static void
1215 menu_chat_im_cb(GtkWidget *w, PidginConversation *gtkconv)
1217 const char *who = g_object_get_data(G_OBJECT(w), "user_data");
1219 chat_do_im(gtkconv, who);
1222 static void
1223 menu_chat_send_file_cb(GtkWidget *w, PidginConversation *gtkconv)
1225 PurpleProtocol *protocol;
1226 PurpleConversation *conv = gtkconv->active_conv;
1227 const char *who = g_object_get_data(G_OBJECT(w), "user_data");
1228 PurpleConnection *gc = purple_conversation_get_connection(conv);
1229 gchar *real_who = NULL;
1231 g_return_if_fail(gc != NULL);
1233 protocol = purple_connection_get_protocol(gc);
1235 if (protocol)
1236 real_who = purple_protocol_chat_iface_get_user_real_name(protocol, gc,
1237 purple_chat_conversation_get_id(PURPLE_CHAT_CONVERSATION(conv)), who);
1239 purple_serv_send_file(gc, real_who ? real_who : who, NULL);
1240 g_free(real_who);
1243 static void
1244 menu_chat_info_cb(GtkWidget *w, PidginConversation *gtkconv)
1246 char *who;
1248 who = g_object_get_data(G_OBJECT(w), "user_data");
1250 chat_do_info(gtkconv, who);
1253 static void
1254 menu_chat_add_remove_cb(GtkWidget *w, PidginConversation *gtkconv)
1256 PurpleConversation *conv = gtkconv->active_conv;
1257 PurpleAccount *account;
1258 PurpleBuddy *b;
1259 char *name;
1261 account = purple_conversation_get_account(conv);
1262 name = g_object_get_data(G_OBJECT(w), "user_data");
1263 b = purple_blist_find_buddy(account, name);
1265 if (b != NULL)
1266 pidgin_dialogs_remove_buddy(b);
1267 else if (account != NULL && purple_account_is_connected(account))
1268 purple_blist_request_add_buddy(account, name, NULL, NULL);
1270 gtk_widget_grab_focus(PIDGIN_CONVERSATION(conv)->entry);
1273 static GtkWidget *
1274 create_chat_menu(PurpleChatConversation *chat, const char *who, PurpleConnection *gc)
1276 static GtkWidget *menu = NULL;
1277 PurpleProtocol *protocol = NULL;
1278 PurpleConversation *conv = PURPLE_CONVERSATION(chat);
1279 PurpleAccount *account = purple_conversation_get_account(conv);
1280 gboolean is_me = FALSE;
1281 GtkWidget *button;
1282 PurpleBuddy *buddy = NULL;
1284 if (gc != NULL)
1285 protocol = purple_connection_get_protocol(gc);
1288 * If a menu already exists, destroy it before creating a new one,
1289 * thus freeing-up the memory it occupied.
1291 if (menu)
1292 gtk_widget_destroy(menu);
1294 if (purple_strequal(purple_chat_conversation_get_nick(chat), purple_normalize(account, who)))
1295 is_me = TRUE;
1297 menu = gtk_menu_new();
1299 if (!is_me) {
1300 button = pidgin_new_menu_item(menu, _("IM"),
1301 PIDGIN_STOCK_TOOLBAR_MESSAGE_NEW,
1302 G_CALLBACK(menu_chat_im_cb),
1303 PIDGIN_CONVERSATION(conv));
1305 if (gc == NULL)
1306 gtk_widget_set_sensitive(button, FALSE);
1307 else
1308 g_object_set_data_full(G_OBJECT(button), "user_data", g_strdup(who), g_free);
1311 if (protocol && PURPLE_IS_PROTOCOL_XFER(protocol))
1313 gboolean can_receive_file = TRUE;
1315 button = pidgin_new_menu_item(menu, _("Send File"),
1316 PIDGIN_STOCK_TOOLBAR_SEND_FILE, G_CALLBACK(menu_chat_send_file_cb),
1317 PIDGIN_CONVERSATION(conv));
1319 if (gc == NULL || protocol == NULL)
1320 can_receive_file = FALSE;
1321 else {
1322 gchar *real_who = NULL;
1323 real_who = purple_protocol_chat_iface_get_user_real_name(protocol, gc,
1324 purple_chat_conversation_get_id(chat), who);
1326 if (!purple_protocol_xfer_can_receive(
1327 PURPLE_PROTOCOL_XFER(protocol),
1328 gc, real_who ? real_who : who)) {
1329 can_receive_file = FALSE;
1332 g_free(real_who);
1335 if (!can_receive_file)
1336 gtk_widget_set_sensitive(button, FALSE);
1337 else
1338 g_object_set_data_full(G_OBJECT(button), "user_data", g_strdup(who), g_free);
1342 if (purple_chat_conversation_is_ignored_user(chat, who))
1343 button = pidgin_new_menu_item(menu, _("Un-Ignore"),
1344 PIDGIN_STOCK_IGNORE, G_CALLBACK(ignore_cb),
1345 PIDGIN_CONVERSATION(conv));
1346 else
1347 button = pidgin_new_menu_item(menu, _("Ignore"),
1348 PIDGIN_STOCK_IGNORE, G_CALLBACK(ignore_cb),
1349 PIDGIN_CONVERSATION(conv));
1351 if (gc == NULL)
1352 gtk_widget_set_sensitive(button, FALSE);
1353 else
1354 g_object_set_data_full(G_OBJECT(button), "user_data", g_strdup(who), g_free);
1357 if (protocol && PURPLE_PROTOCOL_IMPLEMENTS(protocol, SERVER, get_info)) {
1358 button = pidgin_new_menu_item(menu, _("Info"),
1359 PIDGIN_STOCK_TOOLBAR_USER_INFO,
1360 G_CALLBACK(menu_chat_info_cb),
1361 PIDGIN_CONVERSATION(conv));
1363 if (gc == NULL)
1364 gtk_widget_set_sensitive(button, FALSE);
1365 else
1366 g_object_set_data_full(G_OBJECT(button), "user_data", g_strdup(who), g_free);
1369 if (!is_me && protocol && !(purple_protocol_get_options(protocol) & OPT_PROTO_UNIQUE_CHATNAME) && PURPLE_PROTOCOL_IMPLEMENTS(protocol, SERVER, add_buddy)) {
1370 if ((buddy = purple_blist_find_buddy(account, who)) != NULL)
1371 button = pidgin_new_menu_item(menu, _("Remove"),
1372 GTK_STOCK_REMOVE,
1373 G_CALLBACK(menu_chat_add_remove_cb),
1374 PIDGIN_CONVERSATION(conv));
1375 else
1376 button = pidgin_new_menu_item(menu, _("Add"),
1377 GTK_STOCK_ADD,
1378 G_CALLBACK(menu_chat_add_remove_cb),
1379 PIDGIN_CONVERSATION(conv));
1381 if (gc == NULL)
1382 gtk_widget_set_sensitive(button, FALSE);
1383 else
1384 g_object_set_data_full(G_OBJECT(button), "user_data", g_strdup(who), g_free);
1387 if (buddy != NULL)
1389 if (purple_account_is_connected(account))
1390 pidgin_append_blist_node_proto_menu(menu, purple_account_get_connection(account),
1391 (PurpleBlistNode *)buddy);
1392 pidgin_append_blist_node_extended_menu(menu, (PurpleBlistNode *)buddy);
1393 gtk_widget_show_all(menu);
1396 return menu;
1400 static gint
1401 gtkconv_chat_popup_menu_cb(GtkWidget *widget, PidginConversation *gtkconv)
1403 PurpleConversation *conv = gtkconv->active_conv;
1404 PidginChatPane *gtkchat;
1405 PurpleConnection *gc;
1406 PurpleAccount *account;
1407 GtkTreeSelection *sel;
1408 GtkTreeIter iter;
1409 GtkTreeModel *model;
1410 GtkWidget *menu;
1411 gchar *who;
1413 gtkconv = PIDGIN_CONVERSATION(conv);
1414 gtkchat = gtkconv->u.chat;
1415 account = purple_conversation_get_account(conv);
1416 gc = purple_account_get_connection(account);
1418 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
1420 sel = gtk_tree_view_get_selection(GTK_TREE_VIEW(gtkchat->list));
1421 if(!gtk_tree_selection_get_selected(sel, NULL, &iter))
1422 return FALSE;
1424 gtk_tree_model_get(GTK_TREE_MODEL(model), &iter, CHAT_USERS_NAME_COLUMN, &who, -1);
1425 menu = create_chat_menu (PURPLE_CHAT_CONVERSATION(conv), who, gc);
1426 pidgin_menu_popup_at_treeview_selection(menu, widget);
1427 g_free(who);
1429 return TRUE;
1433 static gint
1434 right_click_chat_cb(GtkWidget *widget, GdkEventButton *event,
1435 PidginConversation *gtkconv)
1437 PurpleConversation *conv = gtkconv->active_conv;
1438 PidginChatPane *gtkchat;
1439 PurpleConnection *gc;
1440 PurpleAccount *account;
1441 GtkTreePath *path;
1442 GtkTreeIter iter;
1443 GtkTreeModel *model;
1444 GtkTreeViewColumn *column;
1445 gchar *who;
1446 int x, y;
1448 gtkchat = gtkconv->u.chat;
1449 account = purple_conversation_get_account(conv);
1450 gc = purple_account_get_connection(account);
1452 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
1454 gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(gtkchat->list),
1455 event->x, event->y, &path, &column, &x, &y);
1457 if (path == NULL)
1458 return FALSE;
1460 gtk_tree_selection_select_path(GTK_TREE_SELECTION(
1461 gtk_tree_view_get_selection(GTK_TREE_VIEW(gtkchat->list))), path);
1462 gtk_tree_view_set_cursor(GTK_TREE_VIEW(gtkchat->list),
1463 path, NULL, FALSE);
1464 gtk_widget_grab_focus(GTK_WIDGET(gtkchat->list));
1466 gtk_tree_model_get_iter(GTK_TREE_MODEL(model), &iter, path);
1467 gtk_tree_model_get(GTK_TREE_MODEL(model), &iter, CHAT_USERS_NAME_COLUMN, &who, -1);
1469 /* emit chat-nick-clicked signal */
1470 if (event->type == GDK_BUTTON_PRESS) {
1471 gint plugin_return = GPOINTER_TO_INT(purple_signal_emit_return_1(
1472 pidgin_conversations_get_handle(), "chat-nick-clicked",
1473 conv, who, event->button));
1474 if (plugin_return)
1475 goto handled;
1478 if (event->button == GDK_BUTTON_PRIMARY && event->type == GDK_2BUTTON_PRESS) {
1479 chat_do_im(gtkconv, who);
1480 } else if (gdk_event_triggers_context_menu((GdkEvent *)event)) {
1481 GtkWidget *menu = create_chat_menu (PURPLE_CHAT_CONVERSATION(conv), who, gc);
1482 gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent *)event);
1485 handled:
1486 g_free(who);
1487 gtk_tree_path_free(path);
1489 return TRUE;
1492 static void
1493 activate_list_cb(GtkTreeView *list, GtkTreePath *path, GtkTreeViewColumn *column, PidginConversation *gtkconv)
1495 GtkTreeIter iter;
1496 GtkTreeModel *model;
1497 gchar *who;
1499 model = gtk_tree_view_get_model(GTK_TREE_VIEW(list));
1501 gtk_tree_model_get_iter(GTK_TREE_MODEL(model), &iter, path);
1502 gtk_tree_model_get(GTK_TREE_MODEL(model), &iter, CHAT_USERS_NAME_COLUMN, &who, -1);
1503 chat_do_im(gtkconv, who);
1505 g_free(who);
1508 static void
1509 move_to_next_unread_tab(PidginConversation *gtkconv, gboolean forward)
1511 PidginConversation *next_gtkconv = NULL, *most_active = NULL;
1512 PidginUnseenState unseen_state = PIDGIN_UNSEEN_NONE;
1513 PidginConvWindow *win;
1514 int initial, i, total, diff;
1516 win = gtkconv->win;
1517 initial = gtk_notebook_page_num(GTK_NOTEBOOK(win->notebook),
1518 gtkconv->tab_cont);
1519 total = pidgin_conv_window_get_gtkconv_count(win);
1520 /* By adding total here, the moduli calculated later will always have two
1521 * positive arguments. x % y where x < 0 is not guaranteed to return a
1522 * positive number.
1524 diff = (forward ? 1 : -1) + total;
1526 for (i = (initial + diff) % total; i != initial; i = (i + diff) % total) {
1527 next_gtkconv = pidgin_conv_window_get_gtkconv_at_index(win, i);
1528 if (next_gtkconv->unseen_state > unseen_state) {
1529 most_active = next_gtkconv;
1530 unseen_state = most_active->unseen_state;
1531 if(PIDGIN_UNSEEN_NICK == unseen_state) /* highest possible state */
1532 break;
1536 if (most_active == NULL) { /* no new messages */
1537 i = (i + diff) % total;
1538 most_active = pidgin_conv_window_get_gtkconv_at_index(win, i);
1541 if (most_active != NULL && most_active != gtkconv)
1542 pidgin_conv_window_switch_gtkconv(win, most_active);
1545 static gboolean
1546 gtkconv_cycle_focus(PidginConversation *gtkconv, GtkDirectionType dir)
1548 PurpleConversation *conv = gtkconv->active_conv;
1549 gboolean chat = PURPLE_IS_CHAT_CONVERSATION(conv);
1550 GtkWidget *next = NULL;
1551 struct {
1552 GtkWidget *from;
1553 GtkWidget *to;
1554 } transitions[] = {
1555 {gtkconv->entry, gtkconv->history},
1556 {gtkconv->history, chat ? gtkconv->u.chat->list : gtkconv->entry},
1557 {chat ? gtkconv->u.chat->list : NULL, gtkconv->entry},
1558 {NULL, NULL}
1559 }, *ptr;
1561 for (ptr = transitions; !next && ptr->from; ptr++) {
1562 GtkWidget *from, *to;
1563 if (dir == GTK_DIR_TAB_FORWARD) {
1564 from = ptr->from;
1565 to = ptr->to;
1566 } else {
1567 from = ptr->to;
1568 to = ptr->from;
1570 if (gtk_widget_is_focus(from))
1571 next = to;
1574 if (next)
1575 gtk_widget_grab_focus(next);
1576 return !!next;
1579 static void
1580 update_typing_inserting(PidginConversation *gtkconv)
1582 GtkTextBuffer *buffer = NULL;
1583 gboolean is_empty = FALSE;
1585 g_return_if_fail(gtkconv != NULL);
1587 buffer = talkatu_editor_get_buffer(TALKATU_EDITOR(gtkconv->editor));
1588 is_empty = talkatu_buffer_get_is_empty(TALKATU_BUFFER(buffer));
1590 got_typing_keypress(gtkconv, is_empty);
1593 static gboolean
1594 update_typing_deleting_cb(PidginConversation *gtkconv)
1596 PurpleIMConversation *im = PURPLE_IM_CONVERSATION(gtkconv->active_conv);
1597 GtkTextBuffer *buffer = NULL;
1599 buffer = talkatu_editor_get_buffer(TALKATU_EDITOR(gtkconv->editor));
1601 if (!talkatu_buffer_get_is_empty(TALKATU_BUFFER(buffer))) {
1602 /* We deleted all the text, so turn off typing. */
1603 purple_im_conversation_stop_send_typed_timeout(im);
1605 purple_serv_send_typing(purple_conversation_get_connection(gtkconv->active_conv),
1606 purple_conversation_get_name(gtkconv->active_conv),
1607 PURPLE_IM_NOT_TYPING);
1608 } else {
1609 /* We're deleting, but not all of it, so it counts as typing. */
1610 got_typing_keypress(gtkconv, FALSE);
1613 return FALSE;
1616 static void
1617 update_typing_deleting(PidginConversation *gtkconv)
1619 GtkTextBuffer *buffer = NULL;
1621 g_return_if_fail(gtkconv != NULL);
1623 buffer = talkatu_editor_get_buffer(TALKATU_EDITOR(gtkconv->editor));
1625 if (!talkatu_buffer_get_is_empty(TALKATU_BUFFER(buffer))) {
1626 g_timeout_add(0, (GSourceFunc)update_typing_deleting_cb, gtkconv);
1630 static gboolean
1631 conv_keypress_common(PidginConversation *gtkconv, GdkEventKey *event)
1633 PidginConvWindow *win;
1634 int curconv;
1636 win = gtkconv->win;
1637 curconv = gtk_notebook_get_current_page(GTK_NOTEBOOK(win->notebook));
1639 /* clear any tooltips */
1640 pidgin_tooltip_destroy();
1642 /* If CTRL was held down... */
1643 if (event->state & GDK_CONTROL_MASK) {
1644 switch (event->keyval) {
1645 case GDK_KEY_Page_Down:
1646 case GDK_KEY_KP_Page_Down:
1647 case ']':
1648 if (!pidgin_conv_window_get_gtkconv_at_index(win, curconv + 1))
1649 gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook), 0);
1650 else
1651 gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook), curconv + 1);
1652 return TRUE;
1653 break;
1655 case GDK_KEY_Page_Up:
1656 case GDK_KEY_KP_Page_Up:
1657 case '[':
1658 if (!pidgin_conv_window_get_gtkconv_at_index(win, curconv - 1))
1659 gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook), -1);
1660 else
1661 gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook), curconv - 1);
1662 return TRUE;
1663 break;
1665 case GDK_KEY_Tab:
1666 case GDK_KEY_KP_Tab:
1667 case GDK_KEY_ISO_Left_Tab:
1668 if (event->state & GDK_SHIFT_MASK) {
1669 move_to_next_unread_tab(gtkconv, FALSE);
1670 } else {
1671 move_to_next_unread_tab(gtkconv, TRUE);
1674 return TRUE;
1675 break;
1677 case GDK_KEY_comma:
1678 gtk_notebook_reorder_child(GTK_NOTEBOOK(win->notebook),
1679 gtk_notebook_get_nth_page(GTK_NOTEBOOK(win->notebook), curconv),
1680 curconv - 1);
1681 return TRUE;
1682 break;
1684 case GDK_KEY_period:
1685 gtk_notebook_reorder_child(GTK_NOTEBOOK(win->notebook),
1686 gtk_notebook_get_nth_page(GTK_NOTEBOOK(win->notebook), curconv),
1687 (curconv + 1) % gtk_notebook_get_n_pages(GTK_NOTEBOOK(win->notebook)));
1688 return TRUE;
1689 break;
1690 case GDK_KEY_F6:
1691 if (gtkconv_cycle_focus(gtkconv, event->state & GDK_SHIFT_MASK ? GTK_DIR_TAB_BACKWARD : GTK_DIR_TAB_FORWARD))
1692 return TRUE;
1693 break;
1694 } /* End of switch */
1697 /* If ALT (or whatever) was held down... */
1698 else if (event->state & GDK_MOD1_MASK)
1700 if (event->keyval > '0' && event->keyval <= '9')
1702 guint switchto = event->keyval - '1';
1703 if (switchto < pidgin_conv_window_get_gtkconv_count(win))
1704 gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook), switchto);
1706 return TRUE;
1710 /* If neither CTRL nor ALT were held down... */
1711 else
1713 switch (event->keyval) {
1714 case GDK_KEY_F2:
1715 if (gtk_widget_is_focus(GTK_WIDGET(win->notebook))) {
1716 infopane_entry_activate(gtkconv);
1717 return TRUE;
1719 break;
1720 case GDK_KEY_F6:
1721 if (gtkconv_cycle_focus(gtkconv, event->state & GDK_SHIFT_MASK ? GTK_DIR_TAB_BACKWARD : GTK_DIR_TAB_FORWARD))
1722 return TRUE;
1723 break;
1726 return FALSE;
1729 static gboolean
1730 entry_key_press_cb(GtkWidget *entry, GdkEventKey *event, gpointer data)
1732 PurpleConversation *conv;
1733 PidginConversation *gtkconv;
1735 gtkconv = (PidginConversation *)data;
1736 conv = gtkconv->active_conv;
1738 if (conv_keypress_common(gtkconv, event))
1739 return TRUE;
1741 /* If CTRL was held down... */
1742 if (event->state & GDK_CONTROL_MASK) {
1744 /* If ALT (or whatever) was held down... */
1745 else if (event->state & GDK_MOD1_MASK) {
1748 /* If neither CTRL nor ALT were held down... */
1749 else {
1750 switch (event->keyval) {
1751 case GDK_KEY_Tab:
1752 case GDK_KEY_KP_Tab:
1753 case GDK_KEY_ISO_Left_Tab:
1754 if (gtkconv->entry != entry)
1755 break;
1757 gint plugin_return;
1758 plugin_return = GPOINTER_TO_INT(purple_signal_emit_return_1(
1759 pidgin_conversations_get_handle(), "chat-nick-autocomplete",
1760 conv, event->state & GDK_SHIFT_MASK));
1761 return plugin_return;
1763 break;
1765 case GDK_KEY_Page_Up:
1766 case GDK_KEY_KP_Page_Up:
1767 talkatu_history_page_up(TALKATU_HISTORY(gtkconv->history));
1768 return TRUE;
1769 break;
1771 case GDK_KEY_Page_Down:
1772 case GDK_KEY_KP_Page_Down:
1773 talkatu_history_page_down(TALKATU_HISTORY(gtkconv->history));
1774 return TRUE;
1775 break;
1777 case GDK_KEY_KP_Enter:
1778 case GDK_KEY_Return:
1779 send_cb(entry, gtkconv);
1780 return TRUE;
1781 break;
1786 if (PURPLE_IS_IM_CONVERSATION(conv) &&
1787 purple_prefs_get_bool("/purple/conversations/im/send_typing")) {
1789 switch (event->keyval) {
1790 case GDK_KEY_BackSpace:
1791 case GDK_KEY_Delete:
1792 case GDK_KEY_KP_Delete:
1793 update_typing_deleting(gtkconv);
1794 break;
1795 default:
1796 update_typing_inserting(gtkconv);
1800 return FALSE;
1804 * If someone tries to type into the conversation backlog of a
1805 * conversation window then we yank focus from the conversation backlog
1806 * and give it to the text entry box so that people can type
1807 * all the live long day and it will get entered into the entry box.
1809 static gboolean
1810 refocus_entry_cb(GtkWidget *widget, GdkEventKey *event, gpointer data)
1812 GtkWidget *view = NULL;
1813 PidginConversation *gtkconv = data;
1815 /* If we have a valid key for the conversation display, then exit */
1816 if ((event->state & GDK_CONTROL_MASK) ||
1817 (event->keyval == GDK_KEY_F6) ||
1818 (event->keyval == GDK_KEY_F10) ||
1819 (event->keyval == GDK_KEY_Menu) ||
1820 (event->keyval == GDK_KEY_Shift_L) ||
1821 (event->keyval == GDK_KEY_Shift_R) ||
1822 (event->keyval == GDK_KEY_Control_L) ||
1823 (event->keyval == GDK_KEY_Control_R) ||
1824 (event->keyval == GDK_KEY_Escape) ||
1825 (event->keyval == GDK_KEY_Up) ||
1826 (event->keyval == GDK_KEY_Down) ||
1827 (event->keyval == GDK_KEY_Left) ||
1828 (event->keyval == GDK_KEY_Right) ||
1829 (event->keyval == GDK_KEY_Page_Up) ||
1830 (event->keyval == GDK_KEY_KP_Page_Up) ||
1831 (event->keyval == GDK_KEY_Page_Down) ||
1832 (event->keyval == GDK_KEY_KP_Page_Down) ||
1833 (event->keyval == GDK_KEY_Home) ||
1834 (event->keyval == GDK_KEY_End) ||
1835 (event->keyval == GDK_KEY_Tab) ||
1836 (event->keyval == GDK_KEY_KP_Tab) ||
1837 (event->keyval == GDK_KEY_ISO_Left_Tab))
1839 if (event->type == GDK_KEY_PRESS)
1840 return conv_keypress_common(gtkconv, event);
1841 return FALSE;
1844 view = talkatu_editor_get_view(TALKATU_EDITOR(gtkconv->editor));
1845 gtk_widget_grab_focus(view);
1846 gtk_widget_event(view, (GdkEvent *)event);
1848 return TRUE;
1851 static void
1852 regenerate_options_items(PidginConvWindow *win);
1854 void
1855 pidgin_conv_switch_active_conversation(PurpleConversation *conv)
1857 PidginConversation *gtkconv;
1858 PurpleConversation *old_conv;
1859 PurpleConnectionFlags features;
1861 g_return_if_fail(conv != NULL);
1863 gtkconv = PIDGIN_CONVERSATION(conv);
1864 old_conv = gtkconv->active_conv;
1866 purple_debug_info("gtkconv", "setting active conversation on toolbar %p\n",
1867 conv);
1869 if (old_conv == conv)
1870 return;
1872 purple_conversation_close_logs(old_conv);
1873 gtkconv->active_conv = conv;
1875 purple_conversation_set_logging(conv,
1876 gtk_toggle_action_get_active(GTK_TOGGLE_ACTION(gtkconv->win->menu->logging)));
1878 purple_signal_emit(pidgin_conversations_get_handle(), "conversation-switched", conv);
1880 gray_stuff_out(gtkconv);
1881 update_typing_icon(gtkconv);
1882 g_object_set_data(G_OBJECT(gtkconv->entry), "transient_buddy", NULL);
1883 regenerate_options_items(gtkconv->win);
1885 gtk_window_set_title(GTK_WINDOW(gtkconv->win->window),
1886 gtk_label_get_text(GTK_LABEL(gtkconv->tab_label)));
1889 static void
1890 menu_conv_sel_send_cb(GObject *m, gpointer data)
1892 PurpleAccount *account = g_object_get_data(m, "purple_account");
1893 gchar *name = g_object_get_data(m, "purple_buddy_name");
1894 PurpleIMConversation *im;
1896 if (gtk_check_menu_item_get_active((GtkCheckMenuItem*) m) == FALSE)
1897 return;
1899 im = purple_im_conversation_new(account, name);
1900 pidgin_conv_switch_active_conversation(PURPLE_CONVERSATION(im));
1903 /**************************************************************************
1904 * A bunch of buddy icon functions
1905 **************************************************************************/
1907 static GList *get_protocol_icon_list(PurpleAccount *account)
1909 GList *l = NULL;
1910 PurpleProtocol *protocol =
1911 purple_protocols_find(purple_account_get_protocol_id(account));
1912 const char *protoname = purple_protocol_class_list_icon(protocol, account, NULL);
1913 l = g_hash_table_lookup(protocol_lists, protoname);
1914 if (l)
1915 return l;
1917 l = g_list_prepend(l, pidgin_create_protocol_icon(account, PIDGIN_PROTOCOL_ICON_LARGE));
1918 l = g_list_prepend(l, pidgin_create_protocol_icon(account, PIDGIN_PROTOCOL_ICON_MEDIUM));
1919 l = g_list_prepend(l, pidgin_create_protocol_icon(account, PIDGIN_PROTOCOL_ICON_SMALL));
1921 g_hash_table_insert(protocol_lists, g_strdup(protoname), l);
1922 return l;
1925 static GList *
1926 pidgin_conv_get_tab_icons(PurpleConversation *conv)
1928 PurpleAccount *account = NULL;
1929 const char *name = NULL;
1931 g_return_val_if_fail(conv != NULL, NULL);
1933 account = purple_conversation_get_account(conv);
1934 name = purple_conversation_get_name(conv);
1936 g_return_val_if_fail(account != NULL, NULL);
1937 g_return_val_if_fail(name != NULL, NULL);
1939 /* Use the buddy icon, if possible */
1940 if (PURPLE_IS_IM_CONVERSATION(conv)) {
1941 PurpleBuddy *b = purple_blist_find_buddy(account, name);
1942 if (b != NULL) {
1943 PurplePresence *p;
1944 p = purple_buddy_get_presence(b);
1945 if (purple_presence_is_status_primitive_active(p, PURPLE_STATUS_AWAY))
1946 return away_list;
1947 if (purple_presence_is_status_primitive_active(p, PURPLE_STATUS_UNAVAILABLE))
1948 return busy_list;
1949 if (purple_presence_is_status_primitive_active(p, PURPLE_STATUS_EXTENDED_AWAY))
1950 return xa_list;
1951 if (purple_presence_is_status_primitive_active(p, PURPLE_STATUS_OFFLINE))
1952 return offline_list;
1953 else
1954 return available_list;
1958 return get_protocol_icon_list(account);
1961 static const char *
1962 pidgin_conv_get_icon_stock(PurpleConversation *conv)
1964 PurpleAccount *account = NULL;
1965 const char *stock = NULL;
1967 g_return_val_if_fail(conv != NULL, NULL);
1969 account = purple_conversation_get_account(conv);
1970 g_return_val_if_fail(account != NULL, NULL);
1972 /* Use the buddy icon, if possible */
1973 if (PURPLE_IS_IM_CONVERSATION(conv)) {
1974 const char *name = NULL;
1975 PurpleBuddy *b;
1976 name = purple_conversation_get_name(conv);
1977 b = purple_blist_find_buddy(account, name);
1978 if (b != NULL) {
1979 PurplePresence *p = purple_buddy_get_presence(b);
1980 PurpleStatus *active = purple_presence_get_active_status(p);
1981 PurpleStatusType *type = purple_status_get_status_type(active);
1982 PurpleStatusPrimitive prim = purple_status_type_get_primitive(type);
1983 stock = pidgin_stock_id_from_status_primitive(prim);
1984 } else {
1985 stock = PIDGIN_STOCK_STATUS_PERSON;
1987 } else {
1988 stock = PIDGIN_STOCK_STATUS_CHAT;
1991 return stock;
1994 static GdkPixbuf *
1995 pidgin_conv_get_icon(PurpleConversation *conv, GtkWidget *parent, const char *icon_size)
1997 PurpleAccount *account = NULL;
1998 const char *name = NULL;
1999 const char *stock = NULL;
2000 GdkPixbuf *status = NULL;
2001 GtkIconSize size;
2003 g_return_val_if_fail(conv != NULL, NULL);
2005 account = purple_conversation_get_account(conv);
2006 name = purple_conversation_get_name(conv);
2008 g_return_val_if_fail(account != NULL, NULL);
2009 g_return_val_if_fail(name != NULL, NULL);
2011 /* Use the buddy icon, if possible */
2012 if (PURPLE_IS_IM_CONVERSATION(conv)) {
2013 PurpleBuddy *b = purple_blist_find_buddy(account, name);
2014 if (b != NULL) {
2015 /* I hate this hack. It fixes a bug where the pending message icon
2016 * displays in the conv tab even though it shouldn't.
2017 * A better solution would be great. */
2018 purple_blist_update_node(NULL, PURPLE_BLIST_NODE(b));
2022 stock = pidgin_conv_get_icon_stock(conv);
2023 size = gtk_icon_size_from_name(icon_size);
2024 status = gtk_widget_render_icon (parent, stock, size, "GtkWidget");
2025 return status;
2028 GdkPixbuf *
2029 pidgin_conv_get_tab_icon(PurpleConversation *conv, gboolean small_icon)
2031 const char *icon_size = small_icon ? PIDGIN_ICON_SIZE_TANGO_MICROSCOPIC : PIDGIN_ICON_SIZE_TANGO_EXTRA_SMALL;
2032 return pidgin_conv_get_icon(conv, PIDGIN_CONVERSATION(conv)->icon, icon_size);
2036 static void
2037 update_tab_icon(PurpleConversation *conv)
2039 PidginConversation *gtkconv;
2040 PidginConvWindow *win;
2041 GList *l;
2042 GdkPixbuf *emblem = NULL;
2043 const char *status = NULL;
2044 const char *infopane_status = NULL;
2046 g_return_if_fail(conv != NULL);
2048 gtkconv = PIDGIN_CONVERSATION(conv);
2049 win = gtkconv->win;
2050 if (conv != gtkconv->active_conv)
2051 return;
2053 status = infopane_status = pidgin_conv_get_icon_stock(conv);
2055 if (PURPLE_IS_IM_CONVERSATION(conv)) {
2056 PurpleBuddy *b = purple_blist_find_buddy(purple_conversation_get_account(conv), purple_conversation_get_name(conv));
2057 if (b)
2058 emblem = pidgin_blist_get_emblem((PurpleBlistNode*)b);
2061 g_return_if_fail(status != NULL);
2063 g_object_set(G_OBJECT(gtkconv->icon), "stock", status, NULL);
2064 g_object_set(G_OBJECT(gtkconv->menu_icon), "stock", status, NULL);
2066 gtk_list_store_set(GTK_LIST_STORE(gtkconv->infopane_model),
2067 &(gtkconv->infopane_iter),
2068 CONV_ICON_COLUMN, infopane_status, -1);
2070 gtk_list_store_set(GTK_LIST_STORE(gtkconv->infopane_model),
2071 &(gtkconv->infopane_iter),
2072 CONV_EMBLEM_COLUMN, emblem, -1);
2073 if (emblem)
2074 g_object_unref(emblem);
2076 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/blist/show_protocol_icons")) {
2077 emblem = pidgin_create_protocol_icon(purple_conversation_get_account(gtkconv->active_conv), PIDGIN_PROTOCOL_ICON_SMALL);
2078 } else {
2079 emblem = NULL;
2082 gtk_list_store_set(GTK_LIST_STORE(gtkconv->infopane_model),
2083 &(gtkconv->infopane_iter),
2084 CONV_PROTOCOL_ICON_COLUMN, emblem, -1);
2085 if (emblem)
2086 g_object_unref(emblem);
2088 /* XXX seanegan Why do I have to do this? */
2089 gtk_widget_queue_resize(gtkconv->infopane);
2090 gtk_widget_queue_draw(gtkconv->infopane);
2092 if (pidgin_conv_window_is_active_conversation(conv) &&
2093 (!PURPLE_IS_IM_CONVERSATION(conv) || gtkconv->u.im->anim == NULL))
2095 l = pidgin_conv_get_tab_icons(conv);
2097 gtk_window_set_icon_list(GTK_WINDOW(win->window), l);
2101 static gboolean
2102 redraw_icon(gpointer data)
2104 PidginConversation *gtkconv = (PidginConversation *)data;
2105 PurpleConversation *conv = gtkconv->active_conv;
2106 PurpleAccount *account;
2108 GdkPixbuf *buf;
2109 GdkPixbuf *scale;
2110 gint delay;
2111 int scale_width, scale_height;
2112 int size;
2114 gtkconv = PIDGIN_CONVERSATION(conv);
2115 account = purple_conversation_get_account(conv);
2117 if (!(account && purple_account_get_connection(account))) {
2118 gtkconv->u.im->icon_timer = 0;
2119 return FALSE;
2122 gdk_pixbuf_animation_iter_advance(gtkconv->u.im->iter, NULL);
2123 buf = gdk_pixbuf_animation_iter_get_pixbuf(gtkconv->u.im->iter);
2125 scale_width = gdk_pixbuf_get_width(buf);
2126 scale_height = gdk_pixbuf_get_height(buf);
2128 gtk_widget_get_size_request(gtkconv->u.im->icon_container, NULL, &size);
2129 size = MIN(size, MIN(scale_width, scale_height));
2130 size = CLAMP(size, BUDDYICON_SIZE_MIN, BUDDYICON_SIZE_MAX);
2132 if (scale_width == scale_height) {
2133 scale_width = scale_height = size;
2134 } else if (scale_height > scale_width) {
2135 scale_width = size * scale_width / scale_height;
2136 scale_height = size;
2137 } else {
2138 scale_height = size * scale_height / scale_width;
2139 scale_width = size;
2142 scale = gdk_pixbuf_scale_simple(buf, scale_width, scale_height,
2143 GDK_INTERP_BILINEAR);
2144 if (pidgin_gdk_pixbuf_is_opaque(scale))
2145 pidgin_gdk_pixbuf_make_round(scale);
2147 gtk_image_set_from_pixbuf(GTK_IMAGE(gtkconv->u.im->icon), scale);
2148 g_object_unref(G_OBJECT(scale));
2149 gtk_widget_queue_draw(gtkconv->u.im->icon);
2151 delay = gdk_pixbuf_animation_iter_get_delay_time(gtkconv->u.im->iter);
2153 if (delay < 100)
2154 delay = 100;
2156 gtkconv->u.im->icon_timer = g_timeout_add(delay, redraw_icon, gtkconv);
2158 return FALSE;
2161 static void
2162 start_anim(GtkWidget *widget, PidginConversation *gtkconv)
2164 int delay;
2166 if (gtkconv->u.im->anim == NULL)
2167 return;
2169 if (gtkconv->u.im->icon_timer != 0)
2170 return;
2172 if (gdk_pixbuf_animation_is_static_image(gtkconv->u.im->anim))
2173 return;
2175 delay = gdk_pixbuf_animation_iter_get_delay_time(gtkconv->u.im->iter);
2177 if (delay < 100)
2178 delay = 100;
2180 gtkconv->u.im->icon_timer = g_timeout_add(delay, redraw_icon, gtkconv);
2183 static void
2184 remove_icon(GtkWidget *widget, PidginConversation *gtkconv)
2186 GList *children;
2187 GtkWidget *event;
2188 PurpleConversation *conv = gtkconv->active_conv;
2190 g_return_if_fail(conv != NULL);
2192 gtk_widget_set_size_request(gtkconv->u.im->icon_container, -1, BUDDYICON_SIZE_MIN);
2193 children = gtk_container_get_children(GTK_CONTAINER(gtkconv->u.im->icon_container));
2194 if (children) {
2195 /* We know there's only one child here. It'd be nice to shortcut to the
2196 event box, but we can't change the PidginConversation until 3.0 */
2197 event = (GtkWidget *)children->data;
2198 gtk_container_remove(GTK_CONTAINER(gtkconv->u.im->icon_container), event);
2199 g_list_free(children);
2202 if (gtkconv->u.im->anim != NULL)
2203 g_object_unref(G_OBJECT(gtkconv->u.im->anim));
2205 if (gtkconv->u.im->icon_timer != 0)
2206 g_source_remove(gtkconv->u.im->icon_timer);
2208 if (gtkconv->u.im->iter != NULL)
2209 g_object_unref(G_OBJECT(gtkconv->u.im->iter));
2211 gtkconv->u.im->icon_timer = 0;
2212 gtkconv->u.im->icon = NULL;
2213 gtkconv->u.im->anim = NULL;
2214 gtkconv->u.im->iter = NULL;
2215 gtkconv->u.im->show_icon = FALSE;
2218 static void
2219 saveicon_writefile_cb(void *user_data, const char *filename)
2221 PidginConversation *gtkconv = (PidginConversation *)user_data;
2222 PurpleIMConversation *im = PURPLE_IM_CONVERSATION(gtkconv->active_conv);
2223 PurpleBuddyIcon *icon;
2224 const void *data;
2225 size_t len;
2227 icon = purple_im_conversation_get_icon(im);
2228 data = purple_buddy_icon_get_data(icon, &len);
2230 if ((len <= 0) || (data == NULL) || !purple_util_write_data_to_file_absolute(filename, data, len)) {
2231 purple_notify_error(gtkconv, NULL, _("Unable to save icon file to disk."), NULL, NULL);
2235 static void
2236 custom_icon_sel_cb(const char *filename, gpointer data)
2238 if (filename) {
2239 PurpleContact *contact = data;
2241 purple_buddy_icons_node_set_custom_icon_from_file((PurpleBlistNode*)contact, filename);
2243 g_object_set_data(G_OBJECT(data), "buddy-icon-chooser", NULL);
2246 static void
2247 set_custom_icon_cb(GtkWidget *widget, PurpleContact *contact)
2249 GtkWidget *win = NULL;
2251 /* Should not happen as menu item should be disabled. */
2252 g_return_if_fail(contact != NULL);
2254 win = g_object_get_data(G_OBJECT(contact), "buddy-icon-chooser");
2255 if (win == NULL) {
2256 GtkMenu *menu = GTK_MENU(gtk_widget_get_parent(widget));
2257 GtkWidget *toplevel =
2258 gtk_widget_get_toplevel(gtk_menu_get_attach_widget(menu));
2259 win = pidgin_buddy_icon_chooser_new(GTK_WINDOW(toplevel),
2260 custom_icon_sel_cb, contact);
2261 g_object_set_data_full(G_OBJECT(contact), "buddy-icon-chooser", win,
2262 (GDestroyNotify)gtk_widget_destroy);
2264 gtk_widget_show_all(win);
2267 static void
2268 change_size_cb(GtkWidget *widget, PidginConversation *gtkconv)
2270 int size = 0;
2271 PurpleConversation *conv = gtkconv->active_conv;
2272 GSList *buddies;
2274 gtk_widget_get_size_request(gtkconv->u.im->icon_container, NULL, &size);
2276 if (size == BUDDYICON_SIZE_MAX) {
2277 size = BUDDYICON_SIZE_MIN;
2278 } else {
2279 size = BUDDYICON_SIZE_MAX;
2282 gtk_widget_set_size_request(gtkconv->u.im->icon_container, -1, size);
2283 pidgin_conv_update_buddy_icon(PURPLE_IM_CONVERSATION(conv));
2285 buddies = purple_blist_find_buddies(purple_conversation_get_account(conv),
2286 purple_conversation_get_name(conv));
2287 for (; buddies; buddies = g_slist_delete_link(buddies, buddies)) {
2288 PurpleBuddy *buddy = buddies->data;
2289 PurpleContact *contact = purple_buddy_get_contact(buddy);
2290 purple_blist_node_set_int((PurpleBlistNode*)contact, "pidgin-infopane-iconsize", size);
2294 static void
2295 remove_custom_icon_cb(GtkWidget *widget, PidginConversation *gtkconv)
2297 const gchar *name;
2298 PurpleBuddy *buddy;
2299 PurpleAccount *account;
2300 PurpleContact *contact;
2301 PurpleConversation *conv = gtkconv->active_conv;
2303 account = purple_conversation_get_account(conv);
2304 name = purple_conversation_get_name(conv);
2305 buddy = purple_blist_find_buddy(account, name);
2306 if (!buddy) {
2307 return;
2309 contact = purple_buddy_get_contact(buddy);
2311 purple_buddy_icons_node_set_custom_icon_from_file((PurpleBlistNode*)contact, NULL);
2314 static void
2315 icon_menu_save_cb(GtkWidget *widget, PidginConversation *gtkconv)
2317 PurpleConversation *conv = gtkconv->active_conv;
2318 const gchar *ext;
2319 gchar *buf;
2321 g_return_if_fail(conv != NULL);
2323 ext = purple_buddy_icon_get_extension(purple_im_conversation_get_icon(PURPLE_IM_CONVERSATION(conv)));
2325 buf = g_strdup_printf("%s.%s", purple_normalize(purple_conversation_get_account(conv), purple_conversation_get_name(conv)), ext);
2327 purple_request_file(gtkconv, _("Save Icon"), buf, TRUE,
2328 G_CALLBACK(saveicon_writefile_cb), NULL,
2329 purple_request_cpar_from_conversation(conv), gtkconv);
2331 g_free(buf);
2334 static void
2335 stop_anim(GtkWidget *widget, PidginConversation *gtkconv)
2337 if (gtkconv->u.im->icon_timer != 0)
2338 g_source_remove(gtkconv->u.im->icon_timer);
2340 gtkconv->u.im->icon_timer = 0;
2344 static void
2345 toggle_icon_animate_cb(GtkWidget *w, PidginConversation *gtkconv)
2347 gtkconv->u.im->animate =
2348 gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(w));
2350 if (gtkconv->u.im->animate)
2351 start_anim(NULL, gtkconv);
2352 else
2353 stop_anim(NULL, gtkconv);
2356 static gboolean
2357 icon_menu(GtkWidget *widget, GdkEventButton *e, PidginConversation *gtkconv)
2359 GtkWidget *menu = NULL;
2360 GList *old_menus = NULL;
2361 PurpleConversation *conv;
2362 PurpleBuddy *buddy;
2364 if (e->button == GDK_BUTTON_PRIMARY && e->type == GDK_BUTTON_PRESS) {
2365 change_size_cb(NULL, gtkconv);
2366 return TRUE;
2369 if (!gdk_event_triggers_context_menu((GdkEvent *)e)) {
2370 return FALSE;
2374 * If a menu already exists, destroy it before creating a new one,
2375 * thus freeing-up the memory it occupied.
2377 while ((old_menus = gtk_menu_get_for_attach_widget(widget)) != NULL) {
2378 menu = old_menus->data;
2379 gtk_menu_detach(GTK_MENU(menu));
2380 gtk_widget_destroy(menu);
2383 menu = gtk_menu_new();
2384 gtk_menu_attach_to_widget(GTK_MENU(menu), widget, NULL);
2386 if (gtkconv->u.im->anim &&
2387 !(gdk_pixbuf_animation_is_static_image(gtkconv->u.im->anim)))
2389 pidgin_new_check_item(menu, _("Animate"),
2390 G_CALLBACK(toggle_icon_animate_cb), gtkconv,
2391 gtkconv->u.im->icon_timer);
2394 conv = gtkconv->active_conv;
2395 buddy = purple_blist_find_buddy(purple_conversation_get_account(conv),
2396 purple_conversation_get_name(conv));
2398 pidgin_new_menu_item(menu, _("Hide Icon"), NULL,
2399 G_CALLBACK(remove_icon), gtkconv);
2401 pidgin_new_menu_item(menu, _("Save Icon As..."), GTK_STOCK_SAVE_AS,
2402 G_CALLBACK(icon_menu_save_cb), gtkconv);
2404 if (buddy) {
2405 PurpleContact *contact = purple_buddy_get_contact(buddy);
2406 pidgin_new_menu_item(menu, _("Set Custom Icon..."), NULL,
2407 G_CALLBACK(set_custom_icon_cb), contact);
2408 } else {
2409 GtkWidget *item =
2410 pidgin_new_menu_item(menu, _("Set Custom Icon..."), NULL,
2411 G_CALLBACK(set_custom_icon_cb), NULL);
2412 gtk_widget_set_sensitive(item, FALSE);
2415 pidgin_new_menu_item(menu, _("Change Size"), NULL,
2416 G_CALLBACK(change_size_cb), gtkconv);
2418 /* Is there a custom icon for this person? */
2419 if (buddy)
2421 PurpleContact *contact = purple_buddy_get_contact(buddy);
2422 if (contact && purple_buddy_icons_node_has_custom_icon((PurpleBlistNode*)contact))
2424 pidgin_new_menu_item(menu, _("Remove Custom Icon"),
2425 NULL, G_CALLBACK(remove_custom_icon_cb),
2426 gtkconv);
2430 gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent *)e);
2432 return TRUE;
2435 /**************************************************************************
2436 * End of the bunch of buddy icon functions
2437 **************************************************************************/
2438 void
2439 pidgin_conv_present_conversation(PurpleConversation *conv)
2441 PidginConversation *gtkconv;
2442 GdkModifierType state;
2444 pidgin_conv_attach_to_conversation(conv);
2445 gtkconv = PIDGIN_CONVERSATION(conv);
2447 pidgin_conv_switch_active_conversation(conv);
2448 /* Switch the tab only if the user initiated the event by pressing
2449 * a button or hitting a key. */
2450 if (gtk_get_current_event_state(&state))
2451 pidgin_conv_window_switch_gtkconv(gtkconv->win, gtkconv);
2452 gtk_window_present(GTK_WINDOW(gtkconv->win->window));
2455 static GList *
2456 pidgin_conversations_get_unseen(GList *l,
2457 PidginUnseenState min_state,
2458 gboolean hidden_only,
2459 guint max_count)
2461 GList *r = NULL;
2462 guint c = 0;
2464 for (; l != NULL && (max_count == 0 || c < max_count); l = l->next) {
2465 PurpleConversation *conv = (PurpleConversation*)l->data;
2466 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
2468 if(gtkconv == NULL || gtkconv->active_conv != conv)
2469 continue;
2471 if (gtkconv->unseen_state >= min_state
2472 && (!hidden_only ||
2473 (hidden_only && gtkconv->win == hidden_convwin))) {
2475 r = g_list_prepend(r, conv);
2476 c++;
2480 return r;
2483 GList *
2484 pidgin_conversations_get_unseen_all(PidginUnseenState min_state,
2485 gboolean hidden_only,
2486 guint max_count)
2488 return pidgin_conversations_get_unseen(purple_conversations_get_all(),
2489 min_state, hidden_only, max_count);
2492 GList *
2493 pidgin_conversations_get_unseen_ims(PidginUnseenState min_state,
2494 gboolean hidden_only,
2495 guint max_count)
2497 return pidgin_conversations_get_unseen(purple_conversations_get_ims(),
2498 min_state, hidden_only, max_count);
2501 GList *
2502 pidgin_conversations_get_unseen_chats(PidginUnseenState min_state,
2503 gboolean hidden_only,
2504 guint max_count)
2506 return pidgin_conversations_get_unseen(purple_conversations_get_chats(),
2507 min_state, hidden_only, max_count);
2510 static void
2511 unseen_conv_menu_cb(GtkMenuItem *item, PurpleConversation *conv)
2513 g_return_if_fail(conv != NULL);
2514 pidgin_conv_present_conversation(conv);
2517 static void
2518 unseen_all_conv_menu_cb(GtkMenuItem *item, GList *list)
2520 g_return_if_fail(list != NULL);
2521 /* Do not free the list from here. It will be freed from the
2522 * 'destroy' callback on the menuitem. */
2523 while (list) {
2524 pidgin_conv_present_conversation(list->data);
2525 list = list->next;
2529 guint
2530 pidgin_conversations_fill_menu(GtkWidget *menu, GList *convs)
2532 GList *l;
2533 guint ret=0;
2535 g_return_val_if_fail(menu != NULL, 0);
2536 g_return_val_if_fail(convs != NULL, 0);
2538 for (l = convs; l != NULL ; l = l->next) {
2539 PurpleConversation *conv = (PurpleConversation*)l->data;
2540 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
2542 GtkWidget *icon = gtk_image_new_from_stock(pidgin_conv_get_icon_stock(conv),
2543 gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_MICROSCOPIC));
2544 GtkWidget *item;
2545 gchar *text = g_strdup_printf("%s (%d)",
2546 gtk_label_get_text(GTK_LABEL(gtkconv->tab_label)),
2547 gtkconv->unseen_count);
2549 item = gtk_image_menu_item_new_with_label(text);
2550 gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(item), icon);
2551 g_signal_connect(G_OBJECT(item), "activate", G_CALLBACK(unseen_conv_menu_cb), conv);
2552 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
2553 g_free(text);
2554 ret++;
2557 if (convs->next) {
2558 /* There are more than one conversation. Add an option to show all conversations. */
2559 GtkWidget *item;
2560 GList *list = g_list_copy(convs);
2562 pidgin_separator(menu);
2564 item = gtk_menu_item_new_with_label(_("Show All"));
2565 g_signal_connect(G_OBJECT(item), "activate", G_CALLBACK(unseen_all_conv_menu_cb), list);
2566 g_signal_connect_swapped(G_OBJECT(item), "destroy", G_CALLBACK(g_list_free), list);
2567 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
2570 return ret;
2573 PidginConvWindow *
2574 pidgin_conv_get_window(PidginConversation *gtkconv)
2576 g_return_val_if_fail(gtkconv != NULL, NULL);
2577 return gtkconv->win;
2580 static GtkActionEntry menu_entries[] =
2581 /* TODO: fill out tooltips... */
2583 /* Conversation menu */
2584 { "ConversationMenu", NULL, N_("_Conversation"), NULL, NULL, NULL },
2585 { "NewInstantMessage", PIDGIN_STOCK_TOOLBAR_MESSAGE_NEW, N_("New Instant _Message..."), "<control>M", NULL, G_CALLBACK(menu_new_conv_cb) },
2586 { "JoinAChat", PIDGIN_STOCK_CHAT, N_("Join a _Chat..."), NULL, NULL, G_CALLBACK(menu_join_chat_cb) },
2587 { "Find", GTK_STOCK_FIND, N_("_Find..."), NULL, NULL, G_CALLBACK(menu_find_cb) },
2588 { "ViewLog", NULL, N_("View _Log"), NULL, NULL, G_CALLBACK(menu_view_log_cb) },
2589 { "SaveAs", GTK_STOCK_SAVE_AS, N_("_Save As..."), NULL, NULL, G_CALLBACK(menu_save_as_cb) },
2590 { "ClearScrollback", GTK_STOCK_CLEAR, N_("Clea_r Scrollback"), "<control>L", NULL, G_CALLBACK(menu_clear_cb) },
2592 #ifdef USE_VV
2593 { "MediaMenu", NULL, N_("M_edia"), NULL, NULL, NULL },
2594 { "AudioCall", PIDGIN_STOCK_TOOLBAR_AUDIO_CALL, N_("_Audio Call"), NULL, NULL, G_CALLBACK(menu_initiate_media_call_cb) },
2595 { "VideoCall", PIDGIN_STOCK_TOOLBAR_VIDEO_CALL, N_("_Video Call"), NULL, NULL, G_CALLBACK(menu_initiate_media_call_cb) },
2596 { "AudioVideoCall", PIDGIN_STOCK_TOOLBAR_VIDEO_CALL, N_("Audio/Video _Call"), NULL, NULL, G_CALLBACK(menu_initiate_media_call_cb) },
2597 #endif
2599 { "SendFile", PIDGIN_STOCK_TOOLBAR_SEND_FILE, N_("Se_nd File..."), NULL, NULL, G_CALLBACK(menu_send_file_cb) },
2600 { "GetAttention", PIDGIN_STOCK_TOOLBAR_SEND_ATTENTION, N_("Get _Attention"), NULL, NULL, G_CALLBACK(menu_get_attention_cb) },
2601 { "AddBuddyPounce", NULL, N_("Add Buddy _Pounce..."), NULL, NULL, G_CALLBACK(menu_add_pounce_cb) },
2602 { "GetInfo", PIDGIN_STOCK_TOOLBAR_USER_INFO, N_("_Get Info"), "<control>O", NULL, G_CALLBACK(menu_get_info_cb) },
2603 { "Invite", NULL, N_("In_vite..."), NULL, NULL, G_CALLBACK(menu_invite_cb) },
2604 { "MoreMenu", NULL, N_("M_ore"), NULL, NULL, NULL },
2605 { "Alias", NULL, N_("Al_ias..."), NULL, NULL, G_CALLBACK(menu_alias_cb) },
2606 { "Block", PIDGIN_STOCK_TOOLBAR_BLOCK, N_("_Block..."), NULL, NULL, G_CALLBACK(menu_block_cb) },
2607 { "Unblock", PIDGIN_STOCK_TOOLBAR_UNBLOCK, N_("_Unblock..."), NULL, NULL, G_CALLBACK(menu_unblock_cb) },
2608 { "Add", GTK_STOCK_ADD, N_("_Add..."), NULL, NULL, G_CALLBACK(menu_add_remove_cb) },
2609 { "Remove", GTK_STOCK_REMOVE, N_("_Remove..."), NULL, NULL, G_CALLBACK(menu_add_remove_cb) },
2610 { "InsertLink", PIDGIN_STOCK_TOOLBAR_INSERT_LINK, N_("Insert Lin_k..."), NULL, NULL, NULL },
2611 { "InsertImage", PIDGIN_STOCK_TOOLBAR_INSERT_IMAGE, N_("Insert Imag_e..."), NULL, NULL, NULL },
2612 { "Close", GTK_STOCK_CLOSE, N_("_Close"), "<control>W", NULL, G_CALLBACK(menu_close_conv_cb) },
2614 /* Options */
2615 { "OptionsMenu", NULL, N_("_Options"), NULL, NULL, NULL },
2618 /* Toggle items */
2619 static const GtkToggleActionEntry menu_toggle_entries[] = {
2620 { "EnableLogging", NULL, N_("Enable _Logging"), NULL, NULL, G_CALLBACK(menu_logging_cb), FALSE },
2621 { "EnableSounds", NULL, N_("Enable _Sounds"), NULL, NULL, G_CALLBACK(menu_sounds_cb), FALSE },
2622 { "ShowFormattingToolbars", NULL, N_("Show Formatting _Toolbars"), NULL, NULL, G_CALLBACK(menu_toolbar_cb), FALSE },
2625 static const char *conversation_menu =
2626 "<ui>"
2627 "<menubar name='Conversation'>"
2628 "<menu action='ConversationMenu'>"
2629 "<menuitem action='NewInstantMessage'/>"
2630 "<menuitem action='JoinAChat'/>"
2631 "<separator/>"
2632 "<menuitem action='Find'/>"
2633 "<menuitem action='ViewLog'/>"
2634 "<menuitem action='SaveAs'/>"
2635 "<menuitem action='ClearScrollback'/>"
2636 "<separator/>"
2637 #ifdef USE_VV
2638 "<menu action='MediaMenu'>"
2639 "<menuitem action='AudioCall'/>"
2640 "<menuitem action='VideoCall'/>"
2641 "<menuitem action='AudioVideoCall'/>"
2642 "</menu>"
2643 #endif
2644 "<menuitem action='SendFile'/>"
2645 "<menuitem action='GetAttention'/>"
2646 "<menuitem action='AddBuddyPounce'/>"
2647 "<menuitem action='GetInfo'/>"
2648 "<menuitem action='Invite'/>"
2649 "<menu action='MoreMenu'/>"
2650 "<separator/>"
2651 "<menuitem action='Alias'/>"
2652 "<menuitem action='Block'/>"
2653 "<menuitem action='Unblock'/>"
2654 "<menuitem action='Add'/>"
2655 "<menuitem action='Remove'/>"
2656 "<separator/>"
2657 "<menuitem action='InsertLink'/>"
2658 "<menuitem action='InsertImage'/>"
2659 "<separator/>"
2660 "<menuitem action='Close'/>"
2661 "</menu>"
2662 "<menu action='OptionsMenu'>"
2663 "<menuitem action='EnableLogging'/>"
2664 "<menuitem action='EnableSounds'/>"
2665 "<separator/>"
2666 "<menuitem action='ShowFormattingToolbars'/>"
2667 "</menu>"
2668 "</menubar>"
2669 "</ui>";
2671 static void
2672 sound_method_pref_changed_cb(const char *name, PurplePrefType type,
2673 gconstpointer value, gpointer data)
2675 PidginConvWindow *win = data;
2676 const char *method = value;
2678 if (purple_strequal(method, "none"))
2680 gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(win->menu->sounds),
2681 FALSE);
2682 gtk_action_set_sensitive(win->menu->sounds, FALSE);
2684 else
2686 PidginConversation *gtkconv = pidgin_conv_window_get_active_gtkconv(win);
2688 if (gtkconv != NULL)
2689 gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(win->menu->sounds),
2690 gtkconv->make_sound);
2691 gtk_action_set_sensitive(win->menu->sounds, TRUE);
2695 /* Returns TRUE if some items were added to the menu, FALSE otherwise */
2696 static gboolean
2697 populate_menu_with_options(GtkWidget *menu, PidginConversation *gtkconv, gboolean all)
2699 GList *list;
2700 PurpleConversation *conv;
2701 PurpleAccount *account;
2702 PurpleBlistNode *node = NULL;
2703 PurpleChat *chat = NULL;
2704 PurpleBuddy *buddy = NULL;
2705 gboolean ret;
2707 conv = gtkconv->active_conv;
2708 account = purple_conversation_get_account(conv);
2710 if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
2711 chat = purple_blist_find_chat(account, purple_conversation_get_name(conv));
2713 if ((chat == NULL) && (gtkconv->history != NULL)) {
2714 chat = g_object_get_data(G_OBJECT(gtkconv->history), "transient_chat");
2717 if ((chat == NULL) && (gtkconv->history != NULL)) {
2718 GHashTable *components;
2719 PurpleAccount *account = purple_conversation_get_account(conv);
2720 PurpleProtocol *protocol =
2721 purple_protocols_find(purple_account_get_protocol_id(account));
2722 if (purple_account_get_connection(account) != NULL &&
2723 PURPLE_PROTOCOL_IMPLEMENTS(protocol, CHAT, info_defaults)) {
2724 components = purple_protocol_chat_iface_info_defaults(protocol, purple_account_get_connection(account),
2725 purple_conversation_get_name(conv));
2726 } else {
2727 components = g_hash_table_new_full(g_str_hash, g_str_equal,
2728 g_free, g_free);
2729 g_hash_table_replace(components, g_strdup("channel"),
2730 g_strdup(purple_conversation_get_name(conv)));
2732 chat = purple_chat_new(account, NULL, components);
2733 purple_blist_node_set_transient((PurpleBlistNode *)chat, TRUE);
2734 g_object_set_data_full(G_OBJECT(gtkconv->history), "transient_chat",
2735 chat, (GDestroyNotify)purple_blist_remove_chat);
2737 } else {
2738 if (!purple_account_is_connected(account))
2739 return FALSE;
2741 buddy = purple_blist_find_buddy(account, purple_conversation_get_name(conv));
2742 if (!buddy && gtkconv->history) {
2743 buddy = g_object_get_data(G_OBJECT(gtkconv->history), "transient_buddy");
2745 if (!buddy) {
2746 buddy = purple_buddy_new(account, purple_conversation_get_name(conv), NULL);
2747 purple_blist_node_set_transient((PurpleBlistNode *)buddy, TRUE);
2748 g_object_set_data_full(G_OBJECT(gtkconv->history), "transient_buddy",
2749 buddy, (GDestroyNotify)g_object_unref);
2754 if (chat)
2755 node = (PurpleBlistNode *)chat;
2756 else if (buddy)
2757 node = (PurpleBlistNode *)buddy;
2759 /* Now add the stuff */
2760 if (all) {
2761 if (buddy)
2762 pidgin_blist_make_buddy_menu(menu, buddy, TRUE);
2763 else if (chat) {
2764 /* XXX: */
2766 } else if (node) {
2767 if (purple_account_is_connected(account))
2768 pidgin_append_blist_node_proto_menu(menu, purple_account_get_connection(account), node);
2769 pidgin_append_blist_node_extended_menu(menu, node);
2772 if ((list = gtk_container_get_children(GTK_CONTAINER(menu))) == NULL) {
2773 ret = FALSE;
2774 } else {
2775 g_list_free(list);
2776 ret = TRUE;
2778 return ret;
2781 static void
2782 regenerate_media_items(PidginConvWindow *win)
2784 #ifdef USE_VV
2785 PurpleAccount *account;
2786 PurpleConversation *conv;
2788 conv = pidgin_conv_window_get_active_conversation(win);
2790 if (conv == NULL) {
2791 purple_debug_error("gtkconv", "couldn't get active conversation"
2792 " when regenerating media items\n");
2793 return;
2796 account = purple_conversation_get_account(conv);
2798 if (account == NULL) {
2799 purple_debug_error("gtkconv", "couldn't get account when"
2800 " regenerating media items\n");
2801 return;
2805 * Check if account support voice and/or calls, and
2806 * if the current buddy supports it.
2808 if (account != NULL && PURPLE_IS_IM_CONVERSATION(conv)) {
2809 PurpleMediaCaps caps =
2810 purple_protocol_get_media_caps(account,
2811 purple_conversation_get_name(conv));
2813 gtk_action_set_sensitive(win->menu->audio_call,
2814 caps & PURPLE_MEDIA_CAPS_AUDIO
2815 ? TRUE : FALSE);
2816 gtk_action_set_sensitive(win->menu->video_call,
2817 caps & PURPLE_MEDIA_CAPS_VIDEO
2818 ? TRUE : FALSE);
2819 gtk_action_set_sensitive(win->menu->audio_video_call,
2820 caps & PURPLE_MEDIA_CAPS_AUDIO_VIDEO
2821 ? TRUE : FALSE);
2822 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
2823 /* for now, don't care about chats... */
2824 gtk_action_set_sensitive(win->menu->audio_call, FALSE);
2825 gtk_action_set_sensitive(win->menu->video_call, FALSE);
2826 gtk_action_set_sensitive(win->menu->audio_video_call, FALSE);
2827 } else {
2828 gtk_action_set_sensitive(win->menu->audio_call, FALSE);
2829 gtk_action_set_sensitive(win->menu->video_call, FALSE);
2830 gtk_action_set_sensitive(win->menu->audio_video_call, FALSE);
2832 #endif
2835 static void
2836 regenerate_attention_items(PidginConvWindow *win)
2838 GtkWidget *attention;
2839 GtkWidget *menu;
2840 PurpleConversation *conv;
2841 PurpleConnection *pc;
2842 PurpleProtocol *protocol = NULL;
2843 GList *list;
2845 conv = pidgin_conv_window_get_active_conversation(win);
2846 if (!conv)
2847 return;
2849 attention = gtk_ui_manager_get_widget(win->menu->ui,
2850 "/Conversation/ConversationMenu/GetAttention");
2852 /* Remove the previous entries */
2853 gtk_menu_item_set_submenu(GTK_MENU_ITEM(attention), NULL);
2855 pc = purple_conversation_get_connection(conv);
2856 if (pc != NULL)
2857 protocol = purple_connection_get_protocol(pc);
2859 if (protocol && PURPLE_IS_PROTOCOL_ATTENTION(protocol)) {
2860 list = purple_protocol_attention_get_types(PURPLE_PROTOCOL_ATTENTION(protocol), purple_connection_get_account(pc));
2862 /* Multiple attention types */
2863 if (list && list->next) {
2864 int index = 0;
2866 menu = gtk_menu_new();
2867 while (list) {
2868 PurpleAttentionType *type;
2869 GtkWidget *menuitem;
2871 type = list->data;
2873 menuitem = gtk_menu_item_new_with_label(purple_attention_type_get_name(type));
2874 g_object_set_data(G_OBJECT(menuitem), "index", GINT_TO_POINTER(index));
2875 g_signal_connect(G_OBJECT(menuitem), "activate",
2876 G_CALLBACK(menu_get_attention_cb),
2877 win);
2878 gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
2880 index++;
2881 list = g_list_delete_link(list, list);
2884 gtk_menu_item_set_submenu(GTK_MENU_ITEM(attention), menu);
2885 gtk_widget_show_all(menu);
2890 static void
2891 regenerate_options_items(PidginConvWindow *win)
2893 GtkWidget *menu;
2894 PidginConversation *gtkconv;
2895 GList *list;
2896 GtkWidget *more_menu;
2898 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
2899 more_menu = gtk_ui_manager_get_widget(win->menu->ui,
2900 "/Conversation/ConversationMenu/MoreMenu");
2901 gtk_widget_show(more_menu);
2902 menu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(more_menu));
2904 /* Remove the previous entries */
2905 for (list = gtk_container_get_children(GTK_CONTAINER(menu)); list; )
2907 GtkWidget *w = list->data;
2908 list = g_list_delete_link(list, list);
2909 gtk_widget_destroy(w);
2912 if (!populate_menu_with_options(menu, gtkconv, FALSE))
2914 GtkWidget *item = gtk_menu_item_new_with_label(_("No actions available"));
2915 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
2916 gtk_widget_set_sensitive(item, FALSE);
2919 gtk_widget_show_all(menu);
2922 static void
2923 remove_from_list(GtkWidget *widget, PidginConvWindow *win)
2925 GList *list = g_object_get_data(G_OBJECT(win->window), "plugin-actions");
2926 list = g_list_remove(list, widget);
2927 g_object_set_data(G_OBJECT(win->window), "plugin-actions", list);
2930 static void
2931 regenerate_plugins_items(PidginConvWindow *win)
2933 GList *action_items;
2934 GtkWidget *menu;
2935 GList *list;
2936 PidginConversation *gtkconv;
2937 PurpleConversation *conv;
2938 GtkWidget *item;
2940 if (win->window == NULL || win == hidden_convwin)
2941 return;
2943 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
2944 if (gtkconv == NULL)
2945 return;
2947 conv = gtkconv->active_conv;
2948 action_items = g_object_get_data(G_OBJECT(win->window), "plugin-actions");
2950 /* Remove the old menuitems */
2951 while (action_items) {
2952 g_signal_handlers_disconnect_by_func(G_OBJECT(action_items->data),
2953 G_CALLBACK(remove_from_list), win);
2954 gtk_widget_destroy(action_items->data);
2955 action_items = g_list_delete_link(action_items, action_items);
2958 item = gtk_ui_manager_get_widget(win->menu->ui, "/Conversation/OptionsMenu");
2959 menu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(item));
2961 list = purple_conversation_get_extended_menu(conv);
2962 if (list) {
2963 action_items = g_list_prepend(NULL, (item = pidgin_separator(menu)));
2964 g_signal_connect(G_OBJECT(item), "destroy", G_CALLBACK(remove_from_list), win);
2967 for(; list; list = g_list_delete_link(list, list)) {
2968 PurpleActionMenu *act = (PurpleActionMenu *) list->data;
2969 item = pidgin_append_menu_action(menu, act, conv);
2970 action_items = g_list_prepend(action_items, item);
2971 gtk_widget_show_all(item);
2972 g_signal_connect(G_OBJECT(item), "destroy", G_CALLBACK(remove_from_list), win);
2974 g_object_set_data(G_OBJECT(win->window), "plugin-actions", action_items);
2977 static void menubar_activated(GtkWidget *item, gpointer data)
2979 PidginConvWindow *win = data;
2980 regenerate_media_items(win);
2981 regenerate_options_items(win);
2982 regenerate_plugins_items(win);
2983 regenerate_attention_items(win);
2985 /* The following are to make sure the 'More' submenu is not regenerated every time
2986 * the focus shifts from 'Conversations' to some other menu and back. */
2987 g_signal_handlers_block_by_func(G_OBJECT(item), G_CALLBACK(menubar_activated), data);
2988 g_signal_connect(G_OBJECT(win->menu->menubar), "deactivate", G_CALLBACK(focus_out_from_menubar), data);
2991 static void
2992 focus_out_from_menubar(GtkWidget *wid, PidginConvWindow *win)
2994 /* The menubar has been deactivated. Make sure the 'More' submenu is regenerated next time
2995 * the 'Conversation' menu pops up. */
2996 GtkWidget *menuitem = gtk_ui_manager_get_widget(win->menu->ui, "/Conversation/ConversationMenu");
2997 g_signal_handlers_unblock_by_func(G_OBJECT(menuitem), G_CALLBACK(menubar_activated), win);
2998 g_signal_handlers_disconnect_by_func(G_OBJECT(win->menu->menubar),
2999 G_CALLBACK(focus_out_from_menubar), win);
3002 static GtkWidget *
3003 setup_menubar(PidginConvWindow *win)
3005 GtkAccelGroup *accel_group;
3006 const char *method;
3007 GtkActionGroup *action_group;
3008 GError *error;
3009 GtkWidget *menuitem;
3011 action_group = gtk_action_group_new("ConversationActions");
3012 gtk_action_group_set_translation_domain(action_group, PACKAGE);
3013 gtk_action_group_add_actions(action_group,
3014 menu_entries,
3015 G_N_ELEMENTS(menu_entries),
3016 win);
3017 gtk_action_group_add_toggle_actions(action_group,
3018 menu_toggle_entries,
3019 G_N_ELEMENTS(menu_toggle_entries),
3020 win);
3022 win->menu->ui = gtk_ui_manager_new();
3023 gtk_ui_manager_insert_action_group(win->menu->ui, action_group, 0);
3025 accel_group = gtk_ui_manager_get_accel_group(win->menu->ui);
3026 gtk_window_add_accel_group(GTK_WINDOW(win->window), accel_group);
3027 g_signal_connect(G_OBJECT(accel_group), "accel-changed",
3028 G_CALLBACK(pidgin_save_accels_cb), NULL);
3030 error = NULL;
3031 if (!gtk_ui_manager_add_ui_from_string(win->menu->ui, conversation_menu, -1, &error))
3033 g_message("building menus failed: %s", error->message);
3034 g_error_free(error);
3035 exit(EXIT_FAILURE);
3038 win->menu->menubar =
3039 gtk_ui_manager_get_widget(win->menu->ui, "/Conversation");
3041 /* Make sure the 'Conversation ⇨ More' menuitems are regenerated whenever
3042 * the 'Conversation' menu pops up because the entries can change after the
3043 * conversation is created. */
3044 menuitem = gtk_ui_manager_get_widget(win->menu->ui, "/Conversation/ConversationMenu");
3045 g_signal_connect(G_OBJECT(menuitem), "activate", G_CALLBACK(menubar_activated), win);
3047 win->menu->view_log =
3048 gtk_ui_manager_get_action(win->menu->ui,
3049 "/Conversation/ConversationMenu/ViewLog");
3051 #ifdef USE_VV
3052 win->menu->audio_call =
3053 gtk_ui_manager_get_action(win->menu->ui,
3054 "/Conversation/ConversationMenu/MediaMenu/AudioCall");
3055 win->menu->video_call =
3056 gtk_ui_manager_get_action(win->menu->ui,
3057 "/Conversation/ConversationMenu/MediaMenu/VideoCall");
3058 win->menu->audio_video_call =
3059 gtk_ui_manager_get_action(win->menu->ui,
3060 "/Conversation/ConversationMenu/MediaMenu/AudioVideoCall");
3061 #endif
3063 /* --- */
3065 win->menu->send_file =
3066 gtk_ui_manager_get_action(win->menu->ui,
3067 "/Conversation/ConversationMenu/SendFile");
3069 win->menu->get_attention =
3070 gtk_ui_manager_get_action(win->menu->ui,
3071 "/Conversation/ConversationMenu/GetAttention");
3073 win->menu->add_pounce =
3074 gtk_ui_manager_get_action(win->menu->ui,
3075 "/Conversation/ConversationMenu/AddBuddyPounce");
3077 /* --- */
3079 win->menu->get_info =
3080 gtk_ui_manager_get_action(win->menu->ui,
3081 "/Conversation/ConversationMenu/GetInfo");
3083 win->menu->invite =
3084 gtk_ui_manager_get_action(win->menu->ui,
3085 "/Conversation/ConversationMenu/Invite");
3087 /* --- */
3089 win->menu->alias =
3090 gtk_ui_manager_get_action(win->menu->ui,
3091 "/Conversation/ConversationMenu/Alias");
3093 win->menu->block =
3094 gtk_ui_manager_get_action(win->menu->ui,
3095 "/Conversation/ConversationMenu/Block");
3097 win->menu->unblock =
3098 gtk_ui_manager_get_action(win->menu->ui,
3099 "/Conversation/ConversationMenu/Unblock");
3101 win->menu->add =
3102 gtk_ui_manager_get_action(win->menu->ui,
3103 "/Conversation/ConversationMenu/Add");
3105 win->menu->remove =
3106 gtk_ui_manager_get_action(win->menu->ui,
3107 "/Conversation/ConversationMenu/Remove");
3109 /* --- */
3111 win->menu->insert_link =
3112 gtk_ui_manager_get_action(win->menu->ui,
3113 "/Conversation/ConversationMenu/InsertLink");
3115 win->menu->insert_image =
3116 gtk_ui_manager_get_action(win->menu->ui,
3117 "/Conversation/ConversationMenu/InsertImage");
3119 /* --- */
3121 win->menu->logging =
3122 gtk_ui_manager_get_action(win->menu->ui,
3123 "/Conversation/OptionsMenu/EnableLogging");
3124 win->menu->sounds =
3125 gtk_ui_manager_get_action(win->menu->ui,
3126 "/Conversation/OptionsMenu/EnableSounds");
3127 method = purple_prefs_get_string(PIDGIN_PREFS_ROOT "/sound/method");
3128 if (purple_strequal(method, "none"))
3130 gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(win->menu->sounds),
3131 FALSE);
3132 gtk_action_set_sensitive(win->menu->sounds, FALSE);
3134 purple_prefs_connect_callback(win, PIDGIN_PREFS_ROOT "/sound/method",
3135 sound_method_pref_changed_cb, win);
3137 win->menu->show_formatting_toolbar =
3138 gtk_ui_manager_get_action(win->menu->ui,
3139 "/Conversation/OptionsMenu/ShowFormattingToolbars");
3141 win->menu->tray = pidgin_menu_tray_new();
3142 gtk_menu_shell_append(GTK_MENU_SHELL(win->menu->menubar),
3143 win->menu->tray);
3144 gtk_widget_show(win->menu->tray);
3146 gtk_widget_show(win->menu->menubar);
3148 return win->menu->menubar;
3152 /**************************************************************************
3153 * Utility functions
3154 **************************************************************************/
3156 static void
3157 got_typing_keypress(PidginConversation *gtkconv, gboolean first)
3159 PurpleConversation *conv = gtkconv->active_conv;
3160 PurpleIMConversation *im;
3163 * We know we got something, so we at least have to make sure we don't
3164 * send PURPLE_IM_TYPED any time soon.
3167 im = PURPLE_IM_CONVERSATION(conv);
3169 purple_im_conversation_stop_send_typed_timeout(im);
3170 purple_im_conversation_start_send_typed_timeout(im);
3172 /* Check if we need to send another PURPLE_IM_TYPING message */
3173 if (first || (purple_im_conversation_get_type_again(im) != 0 &&
3174 time(NULL) > purple_im_conversation_get_type_again(im)))
3176 unsigned int timeout;
3177 timeout = purple_serv_send_typing(purple_conversation_get_connection(conv),
3178 purple_conversation_get_name(conv),
3179 PURPLE_IM_TYPING);
3180 purple_im_conversation_set_type_again(im, timeout);
3184 #if 0
3185 static gboolean
3186 typing_animation(gpointer data) {
3187 PidginConversation *gtkconv = data;
3188 PidginConvWindow *gtkwin = gtkconv->win;
3189 const char *stock_id = NULL;
3191 if(gtkconv != pidgin_conv_window_get_active_gtkconv(gtkwin)) {
3192 return FALSE;
3195 switch (rand() % 5) {
3196 case 0:
3197 stock_id = PIDGIN_STOCK_ANIMATION_TYPING0;
3198 break;
3199 case 1:
3200 stock_id = PIDGIN_STOCK_ANIMATION_TYPING1;
3201 break;
3202 case 2:
3203 stock_id = PIDGIN_STOCK_ANIMATION_TYPING2;
3204 break;
3205 case 3:
3206 stock_id = PIDGIN_STOCK_ANIMATION_TYPING3;
3207 break;
3208 case 4:
3209 stock_id = PIDGIN_STOCK_ANIMATION_TYPING4;
3210 break;
3212 if (gtkwin->menu->typing_icon == NULL) {
3213 gtkwin->menu->typing_icon = gtk_image_new_from_stock(stock_id, GTK_ICON_SIZE_MENU);
3214 pidgin_menu_tray_append(PIDGIN_MENU_TRAY(gtkwin->menu->tray),
3215 gtkwin->menu->typing_icon,
3216 _("User is typing..."));
3217 } else {
3218 gtk_image_set_from_stock(GTK_IMAGE(gtkwin->menu->typing_icon), stock_id, GTK_ICON_SIZE_MENU);
3220 gtk_widget_show(gtkwin->menu->typing_icon);
3221 return TRUE;
3223 #endif
3225 static void
3226 update_typing_message(PidginConversation *gtkconv, const char *message)
3228 /* TODO WEBKIT: this is not handled at all */
3229 #if 0
3230 GtkTextBuffer *buffer;
3231 GtkTextMark *stmark, *enmark;
3233 if (g_object_get_data(G_OBJECT(gtkconv->imhtml), "disable-typing-notification"))
3234 return;
3236 buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(gtkconv->imhtml));
3237 stmark = gtk_text_buffer_get_mark(buffer, "typing-notification-start");
3238 enmark = gtk_text_buffer_get_mark(buffer, "typing-notification-end");
3239 if (stmark && enmark) {
3240 GtkTextIter start, end;
3241 gtk_text_buffer_get_iter_at_mark(buffer, &start, stmark);
3242 gtk_text_buffer_get_iter_at_mark(buffer, &end, enmark);
3243 gtk_text_buffer_delete_mark(buffer, stmark);
3244 gtk_text_buffer_delete_mark(buffer, enmark);
3245 gtk_text_buffer_delete(buffer, &start, &end);
3246 } else if (message && *message == '\n' && message[1] == ' ' && message[2] == '\0')
3247 message = NULL;
3249 #ifdef RESERVE_LINE
3250 if (!message)
3251 message = "\n "; /* The blank space is required to avoid a GTK+/Pango bug */
3252 #endif
3254 if (message) {
3255 GtkTextIter iter;
3256 gtk_text_buffer_get_end_iter(buffer, &iter);
3257 gtk_text_buffer_create_mark(buffer, "typing-notification-start", &iter, TRUE);
3258 gtk_text_buffer_insert_with_tags_by_name(buffer, &iter, message, -1, "TYPING-NOTIFICATION", NULL);
3259 gtk_text_buffer_get_end_iter(buffer, &iter);
3260 gtk_text_buffer_create_mark(buffer, "typing-notification-end", &iter, TRUE);
3262 #endif /* if 0 */
3265 static void
3266 update_typing_icon(PidginConversation *gtkconv)
3268 PurpleIMConversation *im;
3269 char *message = NULL;
3271 if (!PURPLE_IS_IM_CONVERSATION(gtkconv->active_conv))
3272 return;
3274 im = PURPLE_IM_CONVERSATION(gtkconv->active_conv);
3276 if (purple_im_conversation_get_typing_state(im) == PURPLE_IM_NOT_TYPING) {
3277 #ifdef RESERVE_LINE
3278 update_typing_message(gtkconv, NULL);
3279 #else
3280 update_typing_message(gtkconv, "\n ");
3281 #endif
3282 return;
3285 if (purple_im_conversation_get_typing_state(im) == PURPLE_IM_TYPING) {
3286 message = g_strdup_printf(_("\n%s is typing..."), purple_conversation_get_title(PURPLE_CONVERSATION(im)));
3287 } else {
3288 message = g_strdup_printf(_("\n%s has stopped typing"), purple_conversation_get_title(PURPLE_CONVERSATION(im)));
3291 update_typing_message(gtkconv, message);
3292 g_free(message);
3295 static gboolean
3296 update_send_to_selection(PidginConvWindow *win)
3298 PurpleAccount *account;
3299 PurpleConversation *conv;
3300 GtkWidget *menu;
3301 GList *child;
3302 PurpleBuddy *b;
3304 conv = pidgin_conv_window_get_active_conversation(win);
3306 if (conv == NULL)
3307 return FALSE;
3309 account = purple_conversation_get_account(conv);
3311 if (account == NULL)
3312 return FALSE;
3314 if (win->menu->send_to == NULL)
3315 return FALSE;
3317 if (!(b = purple_blist_find_buddy(account, purple_conversation_get_name(conv))))
3318 return FALSE;
3320 gtk_widget_show(win->menu->send_to);
3322 menu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(win->menu->send_to));
3324 for (child = gtk_container_get_children(GTK_CONTAINER(menu));
3325 child != NULL;
3326 child = g_list_delete_link(child, child)) {
3328 GtkWidget *item = child->data;
3329 PurpleBuddy *item_buddy;
3330 PurpleAccount *item_account = g_object_get_data(G_OBJECT(item), "purple_account");
3331 gchar *buddy_name = g_object_get_data(G_OBJECT(item),
3332 "purple_buddy_name");
3333 item_buddy = purple_blist_find_buddy(item_account, buddy_name);
3335 if (b == item_buddy) {
3336 gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item), TRUE);
3337 g_list_free(child);
3338 break;
3342 return FALSE;
3345 static gboolean
3346 send_to_item_enter_notify_cb(GtkWidget *menuitem, GdkEventCrossing *event, GtkWidget *label)
3348 gtk_widget_set_sensitive(GTK_WIDGET(label), TRUE);
3349 return FALSE;
3352 static gboolean
3353 send_to_item_leave_notify_cb(GtkWidget *menuitem, GdkEventCrossing *event, GtkWidget *label)
3355 gtk_widget_set_sensitive(GTK_WIDGET(label), FALSE);
3356 return FALSE;
3359 static GtkWidget *
3360 e2ee_state_to_gtkimage(PurpleE2eeState *state)
3362 PurpleImage *img;
3364 img = _pidgin_e2ee_stock_icon_get(
3365 purple_e2ee_state_get_stock_icon(state));
3366 if (!img)
3367 return NULL;
3369 return gtk_image_new_from_pixbuf(pidgin_pixbuf_from_image(img));
3372 static void
3373 create_sendto_item(GtkWidget *menu, GtkSizeGroup *sg, GSList **group,
3374 PurpleBuddy *buddy, PurpleAccount *account, const char *name,
3375 gboolean e2ee_enabled)
3377 GtkWidget *box;
3378 GtkWidget *label;
3379 GtkWidget *image;
3380 GtkWidget *e2ee_image = NULL;
3381 GtkWidget *menuitem;
3382 GdkPixbuf *pixbuf;
3383 gchar *text;
3385 /* Create a pixmap for the protocol icon. */
3386 pixbuf = pidgin_create_protocol_icon(account, PIDGIN_PROTOCOL_ICON_SMALL);
3388 /* Now convert it to GtkImage */
3389 if (pixbuf == NULL)
3390 image = gtk_image_new();
3391 else
3393 image = gtk_image_new_from_pixbuf(pixbuf);
3394 g_object_unref(G_OBJECT(pixbuf));
3397 if (e2ee_enabled) {
3398 PurpleIMConversation *im;
3399 PurpleE2eeState *state = NULL;
3401 im = purple_conversations_find_im_with_account(
3402 purple_buddy_get_name(buddy), purple_buddy_get_account(buddy));
3403 if (im)
3404 state = purple_conversation_get_e2ee_state(PURPLE_CONVERSATION(im));
3405 if (state)
3406 e2ee_image = e2ee_state_to_gtkimage(state);
3407 else
3408 e2ee_image = gtk_image_new();
3411 gtk_size_group_add_widget(sg, image);
3413 /* Make our menu item */
3414 text = g_strdup_printf("%s (%s)", name, purple_account_get_name_for_display(account));
3415 menuitem = gtk_radio_menu_item_new_with_label(*group, text);
3416 g_free(text);
3417 *group = gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(menuitem));
3419 /* Do some evil, see some evil, speak some evil. */
3420 box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
3422 label = gtk_bin_get_child(GTK_BIN(menuitem));
3423 g_object_ref(label);
3424 gtk_container_remove(GTK_CONTAINER(menuitem), label);
3426 gtk_box_pack_start(GTK_BOX(box), image, FALSE, FALSE, 0);
3428 gtk_box_pack_start(GTK_BOX(box), label, TRUE, TRUE, 4);
3429 if (e2ee_image)
3430 gtk_box_pack_start(GTK_BOX(box), e2ee_image, FALSE, FALSE, 0);
3432 if (buddy != NULL &&
3433 !purple_presence_is_online(purple_buddy_get_presence(buddy)))
3435 gtk_widget_set_sensitive(label, FALSE);
3437 /* Set the label sensitive when the menuitem is highlighted and
3438 * insensitive again when the mouse leaves it. This way, it
3439 * doesn't appear weird from the highlighting of the embossed
3440 * (insensitive style) text.*/
3441 g_signal_connect(menuitem, "enter-notify-event",
3442 G_CALLBACK(send_to_item_enter_notify_cb), label);
3443 g_signal_connect(menuitem, "leave-notify-event",
3444 G_CALLBACK(send_to_item_leave_notify_cb), label);
3447 g_object_unref(label);
3449 gtk_container_add(GTK_CONTAINER(menuitem), box);
3451 gtk_widget_show(label);
3452 gtk_widget_show(image);
3453 if (e2ee_image)
3454 gtk_widget_show(e2ee_image);
3455 gtk_widget_show(box);
3457 /* Set our data and callbacks. */
3458 g_object_set_data(G_OBJECT(menuitem), "purple_account", account);
3459 g_object_set_data_full(G_OBJECT(menuitem), "purple_buddy_name", g_strdup(name), g_free);
3461 g_signal_connect(G_OBJECT(menuitem), "activate",
3462 G_CALLBACK(menu_conv_sel_send_cb), NULL);
3464 gtk_widget_show(menuitem);
3465 gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
3468 static gboolean
3469 compare_buddy_presence(PurplePresence *p1, PurplePresence *p2)
3471 /* This is necessary because multiple PurpleBuddy's don't share the same
3472 * PurplePresence anymore.
3474 PurpleBuddy *b1 = purple_buddy_presence_get_buddy(PURPLE_BUDDY_PRESENCE(p1));
3475 PurpleBuddy *b2 = purple_buddy_presence_get_buddy(PURPLE_BUDDY_PRESENCE(p2));
3476 if (purple_buddy_get_account(b1) == purple_buddy_get_account(b2) &&
3477 purple_strequal(purple_buddy_get_name(b1), purple_buddy_get_name(b2)))
3478 return FALSE;
3479 return TRUE;
3482 static void
3483 generate_send_to_items(PidginConvWindow *win)
3485 GtkWidget *menu;
3486 GSList *group = NULL;
3487 GtkSizeGroup *sg = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL);
3488 PidginConversation *gtkconv;
3489 GSList *l, *buds;
3491 g_return_if_fail(win != NULL);
3493 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
3495 g_return_if_fail(gtkconv != NULL);
3497 if (win->menu->send_to != NULL)
3498 gtk_widget_destroy(win->menu->send_to);
3500 /* Build the Send To menu */
3501 win->menu->send_to = gtk_menu_item_new_with_mnemonic(_("S_end To"));
3502 gtk_widget_show(win->menu->send_to);
3504 menu = gtk_menu_new();
3505 gtk_menu_shell_insert(GTK_MENU_SHELL(win->menu->menubar),
3506 win->menu->send_to, 2);
3507 gtk_menu_item_set_submenu(GTK_MENU_ITEM(win->menu->send_to), menu);
3509 gtk_widget_show(menu);
3511 if (PURPLE_IS_IM_CONVERSATION(gtkconv->active_conv)) {
3512 buds = purple_blist_find_buddies(purple_conversation_get_account(gtkconv->active_conv), purple_conversation_get_name(gtkconv->active_conv));
3514 if (buds == NULL)
3516 /* The user isn't on the buddy list. So we don't create any sendto menu. */
3518 else
3520 gboolean e2ee_enabled = FALSE;
3521 GList *list = NULL, *iter;
3522 for (l = buds; l != NULL; l = l->next)
3524 PurpleBlistNode *node;
3526 node = PURPLE_BLIST_NODE(purple_buddy_get_contact(PURPLE_BUDDY(l->data)));
3528 for (node = node->child; node != NULL; node = node->next)
3530 PurpleBuddy *buddy = (PurpleBuddy *)node;
3531 PurpleAccount *account;
3532 PurpleIMConversation *im;
3534 if (!PURPLE_IS_BUDDY(node))
3535 continue;
3537 im = purple_conversations_find_im_with_account(purple_buddy_get_name(buddy), purple_buddy_get_account(buddy));
3538 if (im && purple_conversation_get_e2ee_state(PURPLE_CONVERSATION(im)) != NULL)
3539 e2ee_enabled = TRUE;
3541 account = purple_buddy_get_account(buddy);
3542 /* TODO WEBKIT: (I'm not actually sure if this is webkit-related --Mark Doliner) */
3543 if (purple_account_is_connected(account) /*|| account == purple_conversation_get_account(gtkconv->active_conv)*/)
3545 /* Use the PurplePresence to get unique buddies. */
3546 PurplePresence *presence = purple_buddy_get_presence(buddy);
3547 if (!g_list_find_custom(list, presence, (GCompareFunc)compare_buddy_presence))
3548 list = g_list_prepend(list, presence);
3553 /* Create the sendto menu only if it has more than one item to show */
3554 if (list && list->next) {
3555 /* Loop over the list backwards so we get the items in the right order,
3556 * since we did a g_list_prepend() earlier. */
3557 for (iter = g_list_last(list); iter != NULL; iter = iter->prev) {
3558 PurplePresence *pre = iter->data;
3559 PurpleBuddy *buddy = purple_buddy_presence_get_buddy(PURPLE_BUDDY_PRESENCE(pre));
3560 create_sendto_item(menu, sg, &group, buddy,
3561 purple_buddy_get_account(buddy), purple_buddy_get_name(buddy), e2ee_enabled);
3564 g_list_free(list);
3565 g_slist_free(buds);
3569 g_object_unref(sg);
3571 gtk_widget_show(win->menu->send_to);
3572 /* TODO: This should never be insensitive. Possibly hidden or not. */
3573 if (!group)
3574 gtk_widget_set_sensitive(win->menu->send_to, FALSE);
3575 update_send_to_selection(win);
3578 PurpleImage *
3579 _pidgin_e2ee_stock_icon_get(const gchar *stock_name)
3581 gchar filename[100], *path;
3582 PurpleImage *image;
3584 /* core is quitting */
3585 if (e2ee_stock == NULL)
3586 return NULL;
3588 if (g_hash_table_lookup_extended(e2ee_stock, stock_name, NULL, (gpointer*)&image))
3589 return image;
3591 g_snprintf(filename, sizeof(filename), "e2ee-%s.png", stock_name);
3592 path = g_build_filename(PURPLE_DATADIR, "pidgin", "icons",
3593 "hicolor", "16x16", "status", filename, NULL);
3594 image = purple_image_new_from_file(path, NULL);
3595 g_free(path);
3597 g_hash_table_insert(e2ee_stock, g_strdup(stock_name), image);
3598 return image;
3601 static void
3602 generate_e2ee_controls(PidginConvWindow *win)
3604 PidginConversation *gtkconv;
3605 PurpleConversation *conv;
3606 PurpleE2eeState *state;
3607 PurpleE2eeProvider *provider;
3608 GtkWidget *menu;
3609 GList *menu_actions, *it;
3610 GtkWidget *e2ee_image;
3612 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
3613 g_return_if_fail(gtkconv != NULL);
3615 conv = gtkconv->active_conv;
3616 g_return_if_fail(conv != NULL);
3618 if (win->menu->e2ee != NULL) {
3619 gtk_widget_destroy(win->menu->e2ee);
3620 win->menu->e2ee = NULL;
3623 provider = purple_e2ee_provider_get_main();
3624 state = purple_conversation_get_e2ee_state(conv);
3625 if (state == NULL || provider == NULL)
3626 return;
3627 if (purple_e2ee_state_get_provider(state) != provider)
3628 return;
3630 win->menu->e2ee = gtk_image_menu_item_new_with_label(
3631 purple_e2ee_provider_get_name(provider));
3633 menu = gtk_menu_new();
3634 gtk_menu_shell_insert(GTK_MENU_SHELL(win->menu->menubar),
3635 win->menu->e2ee, 3);
3636 gtk_menu_item_set_submenu(GTK_MENU_ITEM(win->menu->e2ee), menu);
3638 e2ee_image = e2ee_state_to_gtkimage(state);
3639 if (e2ee_image) {
3640 gtk_image_menu_item_set_image(
3641 GTK_IMAGE_MENU_ITEM(win->menu->e2ee), e2ee_image);
3644 gtk_widget_set_tooltip_text(win->menu->e2ee,
3645 purple_e2ee_state_get_name(state));
3647 menu_actions = purple_e2ee_provider_get_conv_menu_actions(provider, conv);
3648 for (it = menu_actions; it; it = g_list_next(it)) {
3649 PurpleActionMenu *action = it->data;
3651 gtk_widget_show_all(
3652 pidgin_append_menu_action(menu, action, conv));
3654 g_list_free(menu_actions);
3656 gtk_widget_show(win->menu->e2ee);
3657 gtk_widget_show(menu);
3660 static const char *
3661 get_chat_user_status_icon(PurpleChatConversation *chat, const char *name, PurpleChatUserFlags flags)
3663 const char *image = NULL;
3665 if (flags & PURPLE_CHAT_USER_FOUNDER) {
3666 image = PIDGIN_STOCK_STATUS_FOUNDER;
3667 } else if (flags & PURPLE_CHAT_USER_OP) {
3668 image = PIDGIN_STOCK_STATUS_OPERATOR;
3669 } else if (flags & PURPLE_CHAT_USER_HALFOP) {
3670 image = PIDGIN_STOCK_STATUS_HALFOP;
3671 } else if (flags & PURPLE_CHAT_USER_VOICE) {
3672 image = PIDGIN_STOCK_STATUS_VOICE;
3673 } else if ((!flags) && purple_chat_conversation_is_ignored_user(chat, name)) {
3674 image = PIDGIN_STOCK_STATUS_IGNORED;
3675 } else {
3676 return NULL;
3678 return image;
3681 static void
3682 deleting_chat_user_cb(PurpleChatUser *cb)
3684 GtkTreeRowReference *ref = purple_chat_user_get_ui_data(cb);
3686 if (ref) {
3687 gtk_tree_row_reference_free(ref);
3688 purple_chat_user_set_ui_data(cb, NULL);
3692 static void
3693 add_chat_user_common(PurpleChatConversation *chat, PurpleChatUser *cb, const char *old_name)
3695 PidginConversation *gtkconv;
3696 PurpleConversation *conv;
3697 PidginChatPane *gtkchat;
3698 PurpleConnection *gc;
3699 PurpleProtocol *protocol;
3700 GtkTreeModel *tm;
3701 GtkListStore *ls;
3702 GtkTreePath *newpath;
3703 const char *stock;
3704 GtkTreeIter iter;
3705 gboolean is_me = FALSE;
3706 gboolean is_buddy;
3707 const gchar *name, *alias;
3708 gchar *tmp, *alias_key;
3709 PurpleChatUserFlags flags;
3710 GdkRGBA *color = NULL;
3712 alias = purple_chat_user_get_alias(cb);
3713 name = purple_chat_user_get_name(cb);
3714 flags = purple_chat_user_get_flags(cb);
3716 conv = PURPLE_CONVERSATION(chat);
3717 gtkconv = PIDGIN_CONVERSATION(conv);
3718 gtkchat = gtkconv->u.chat;
3719 gc = purple_conversation_get_connection(conv);
3721 if (!gc || !(protocol = purple_connection_get_protocol(gc)))
3722 return;
3724 tm = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
3725 ls = GTK_LIST_STORE(tm);
3727 stock = get_chat_user_status_icon(chat, name, flags);
3729 if (purple_strequal(purple_chat_conversation_get_nick(chat), purple_normalize(purple_conversation_get_account(conv), old_name != NULL ? old_name : name)))
3730 is_me = TRUE;
3732 is_buddy = purple_chat_user_is_buddy(cb);
3734 tmp = g_utf8_casefold(alias, -1);
3735 alias_key = g_utf8_collate_key(tmp, -1);
3736 g_free(tmp);
3738 if (is_me) {
3739 #if 0
3740 /* TODO WEBKIT: No tags in webkit stuff, yet. */
3741 GtkTextTag *tag = gtk_text_tag_table_lookup(
3742 gtk_text_buffer_get_tag_table(GTK_IMHTML(gtkconv->webview)->text_buffer),
3743 "send-name");
3744 g_object_get(tag, "foreground-rgba", &color, NULL);
3745 #endif /* if 0 */
3746 } else {
3747 GtkTextTag *tag;
3748 if ((tag = get_buddy_tag(chat, name, 0, FALSE)))
3749 g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_NORMAL, NULL);
3750 if ((tag = get_buddy_tag(chat, name, PURPLE_MESSAGE_NICK, FALSE)))
3751 g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_NORMAL, NULL);
3752 color = (GdkRGBA*)get_nick_color(gtkconv, name);
3755 gtk_list_store_insert_with_values(ls, &iter,
3757 * The GTK docs are mute about the effects of the "row" value for performance.
3758 * X-Chat hardcodes their value to 0 (prepend) and -1 (append), so we will too.
3759 * It *might* be faster to search the gtk_list_store and set row accurately,
3760 * but no one in #gtk+ seems to know anything about it either.
3761 * Inserting in the "wrong" location has no visible ill effects. - F.P.
3763 -1, /* "row" */
3764 CHAT_USERS_ICON_STOCK_COLUMN, stock,
3765 CHAT_USERS_ALIAS_COLUMN, alias,
3766 CHAT_USERS_ALIAS_KEY_COLUMN, alias_key,
3767 CHAT_USERS_NAME_COLUMN, name,
3768 CHAT_USERS_FLAGS_COLUMN, flags,
3769 CHAT_USERS_COLOR_COLUMN, color,
3770 CHAT_USERS_WEIGHT_COLUMN, is_buddy ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL,
3771 -1);
3773 if (purple_chat_user_get_ui_data(cb)) {
3774 GtkTreeRowReference *ref = purple_chat_user_get_ui_data(cb);
3775 gtk_tree_row_reference_free(ref);
3778 newpath = gtk_tree_model_get_path(tm, &iter);
3779 purple_chat_user_set_ui_data(cb, gtk_tree_row_reference_new(tm, newpath));
3780 gtk_tree_path_free(newpath);
3782 #if 0
3783 if (is_me && color)
3784 gdk_rgba_free(color);
3785 #endif
3786 g_free(alias_key);
3789 static void topic_callback(GtkWidget *w, PidginConversation *gtkconv)
3791 PurpleProtocol *protocol = NULL;
3792 PurpleConnection *gc;
3793 PurpleConversation *conv = gtkconv->active_conv;
3794 PidginChatPane *gtkchat;
3795 char *new_topic;
3796 const char *current_topic;
3798 gc = purple_conversation_get_connection(conv);
3800 if(!gc || !(protocol = purple_connection_get_protocol(gc)))
3801 return;
3803 if(!PURPLE_PROTOCOL_IMPLEMENTS(protocol, CHAT, set_topic))
3804 return;
3806 gtkconv = PIDGIN_CONVERSATION(conv);
3807 gtkchat = gtkconv->u.chat;
3808 new_topic = g_strdup(gtk_entry_get_text(GTK_ENTRY(gtkchat->topic_text)));
3809 current_topic = purple_chat_conversation_get_topic(PURPLE_CHAT_CONVERSATION(conv));
3811 if(current_topic && !g_utf8_collate(new_topic, current_topic)){
3812 g_free(new_topic);
3813 return;
3816 if (current_topic)
3817 gtk_entry_set_text(GTK_ENTRY(gtkchat->topic_text), current_topic);
3818 else
3819 gtk_entry_set_text(GTK_ENTRY(gtkchat->topic_text), "");
3821 purple_protocol_chat_iface_set_topic(protocol, gc, purple_chat_conversation_get_id(PURPLE_CHAT_CONVERSATION(conv)),
3822 new_topic);
3824 g_free(new_topic);
3827 static gint
3828 sort_chat_users(GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, gpointer userdata)
3830 PurpleChatUserFlags f1 = 0, f2 = 0;
3831 char *user1 = NULL, *user2 = NULL;
3832 gboolean buddy1 = FALSE, buddy2 = FALSE;
3833 gint ret = 0;
3835 gtk_tree_model_get(model, a,
3836 CHAT_USERS_ALIAS_KEY_COLUMN, &user1,
3837 CHAT_USERS_FLAGS_COLUMN, &f1,
3838 CHAT_USERS_WEIGHT_COLUMN, &buddy1,
3839 -1);
3840 gtk_tree_model_get(model, b,
3841 CHAT_USERS_ALIAS_KEY_COLUMN, &user2,
3842 CHAT_USERS_FLAGS_COLUMN, &f2,
3843 CHAT_USERS_WEIGHT_COLUMN, &buddy2,
3844 -1);
3846 /* Only sort by membership levels */
3847 f1 &= PURPLE_CHAT_USER_VOICE | PURPLE_CHAT_USER_HALFOP | PURPLE_CHAT_USER_OP |
3848 PURPLE_CHAT_USER_FOUNDER;
3849 f2 &= PURPLE_CHAT_USER_VOICE | PURPLE_CHAT_USER_HALFOP | PURPLE_CHAT_USER_OP |
3850 PURPLE_CHAT_USER_FOUNDER;
3852 ret = g_strcmp0(user1, user2);
3854 if (user1 != NULL && user2 != NULL) {
3855 if (f1 != f2) {
3856 /* sort more important users first */
3857 ret = (f1 > f2) ? -1 : 1;
3858 } else if (buddy1 != buddy2) {
3859 ret = (buddy1 > buddy2) ? -1 : 1;
3863 g_free(user1);
3864 g_free(user2);
3866 return ret;
3869 static void
3870 update_chat_alias(PurpleBuddy *buddy, PurpleChatConversation *chat, PurpleConnection *gc, PurpleProtocol *protocol)
3872 PidginConversation *gtkconv = PIDGIN_CONVERSATION(PURPLE_CONVERSATION(chat));
3873 PurpleAccount *account = purple_conversation_get_account(PURPLE_CONVERSATION(chat));
3874 GtkTreeModel *model;
3875 char *normalized_name;
3876 GtkTreeIter iter;
3877 int f;
3879 g_return_if_fail(buddy != NULL);
3880 g_return_if_fail(chat != NULL);
3882 /* This is safe because this callback is only used in chats, not IMs. */
3883 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkconv->u.chat->list));
3885 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &iter))
3886 return;
3888 normalized_name = g_strdup(purple_normalize(account, purple_buddy_get_name(buddy)));
3890 do {
3891 char *name;
3893 gtk_tree_model_get(model, &iter, CHAT_USERS_NAME_COLUMN, &name, -1);
3895 if (purple_strequal(normalized_name, purple_normalize(account, name))) {
3896 const char *alias = name;
3897 char *tmp;
3898 char *alias_key = NULL;
3899 PurpleBuddy *buddy2;
3901 if (!purple_strequal(purple_chat_conversation_get_nick(chat), purple_normalize(account, name))) {
3902 /* This user is not me, so look into updating the alias. */
3904 if ((buddy2 = purple_blist_find_buddy(account, name)) != NULL) {
3905 alias = purple_buddy_get_contact_alias(buddy2);
3908 tmp = g_utf8_casefold(alias, -1);
3909 alias_key = g_utf8_collate_key(tmp, -1);
3910 g_free(tmp);
3912 gtk_list_store_set(GTK_LIST_STORE(model), &iter,
3913 CHAT_USERS_ALIAS_COLUMN, alias,
3914 CHAT_USERS_ALIAS_KEY_COLUMN, alias_key,
3915 -1);
3916 g_free(alias_key);
3918 g_free(name);
3919 break;
3922 f = gtk_tree_model_iter_next(model, &iter);
3924 g_free(name);
3925 } while (f != 0);
3927 g_free(normalized_name);
3930 static void
3931 blist_node_aliased_cb(PurpleBlistNode *node, const char *old_alias, PurpleChatConversation *chat)
3933 PurpleConnection *gc;
3934 PurpleProtocol *protocol;
3935 PurpleConversation *conv = PURPLE_CONVERSATION(chat);
3937 g_return_if_fail(node != NULL);
3938 g_return_if_fail(conv != NULL);
3940 gc = purple_conversation_get_connection(conv);
3941 g_return_if_fail(gc != NULL);
3942 g_return_if_fail(purple_connection_get_protocol(gc) != NULL);
3943 protocol = purple_connection_get_protocol(gc);
3945 if (purple_protocol_get_options(protocol) & OPT_PROTO_UNIQUE_CHATNAME)
3946 return;
3948 if (PURPLE_IS_CONTACT(node))
3950 PurpleBlistNode *bnode;
3952 for(bnode = node->child; bnode; bnode = bnode->next) {
3954 if(!PURPLE_IS_BUDDY(bnode))
3955 continue;
3957 update_chat_alias((PurpleBuddy *)bnode, chat, gc, protocol);
3960 else if (PURPLE_IS_BUDDY(node))
3961 update_chat_alias((PurpleBuddy *)node, chat, gc, protocol);
3962 else if (PURPLE_IS_CHAT(node) &&
3963 purple_conversation_get_account(conv) == purple_chat_get_account((PurpleChat*)node))
3965 if (old_alias == NULL || g_utf8_collate(old_alias, purple_conversation_get_title(conv)) == 0)
3966 pidgin_conv_update_fields(conv, PIDGIN_CONV_SET_TITLE);
3970 static void
3971 buddy_cb_common(PurpleBuddy *buddy, PurpleChatConversation *chat, gboolean is_buddy)
3973 GtkTreeModel *model;
3974 char *normalized_name;
3975 GtkTreeIter iter;
3976 GtkTextTag *texttag;
3977 PurpleConversation *conv = PURPLE_CONVERSATION(chat);
3978 int f;
3980 g_return_if_fail(buddy != NULL);
3981 g_return_if_fail(conv != NULL);
3983 /* Do nothing if the buddy does not belong to the conv's account */
3984 if (purple_buddy_get_account(buddy) != purple_conversation_get_account(conv))
3985 return;
3987 /* This is safe because this callback is only used in chats, not IMs. */
3988 model = gtk_tree_view_get_model(GTK_TREE_VIEW(PIDGIN_CONVERSATION(conv)->u.chat->list));
3990 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &iter))
3991 return;
3993 normalized_name = g_strdup(purple_normalize(purple_conversation_get_account(conv), purple_buddy_get_name(buddy)));
3995 do {
3996 char *name;
3998 gtk_tree_model_get(model, &iter, CHAT_USERS_NAME_COLUMN, &name, -1);
4000 if (purple_strequal(normalized_name, purple_normalize(purple_conversation_get_account(conv), name))) {
4001 gtk_list_store_set(GTK_LIST_STORE(model), &iter,
4002 CHAT_USERS_WEIGHT_COLUMN, is_buddy ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL, -1);
4003 g_free(name);
4004 break;
4007 f = gtk_tree_model_iter_next(model, &iter);
4009 g_free(name);
4010 } while (f != 0);
4012 g_free(normalized_name);
4014 blist_node_aliased_cb((PurpleBlistNode *)buddy, NULL, chat);
4016 texttag = get_buddy_tag(chat, purple_buddy_get_name(buddy), 0, FALSE); /* XXX: do we want the normalized name? */
4017 if (texttag) {
4018 g_object_set(texttag, "weight", is_buddy ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL, NULL);
4022 static void
4023 buddy_added_cb(PurpleBlistNode *node, PurpleChatConversation *chat)
4025 if (!PURPLE_IS_BUDDY(node))
4026 return;
4028 buddy_cb_common(PURPLE_BUDDY(node), chat, TRUE);
4031 static void
4032 buddy_removed_cb(PurpleBlistNode *node, PurpleChatConversation *chat)
4034 if (!PURPLE_IS_BUDDY(node))
4035 return;
4037 /* If there's another buddy for the same "dude" on the list, do nothing. */
4038 if (purple_blist_find_buddy(purple_buddy_get_account(PURPLE_BUDDY(node)),
4039 purple_buddy_get_name(PURPLE_BUDDY(node))) != NULL)
4040 return;
4042 buddy_cb_common(PURPLE_BUDDY(node), chat, FALSE);
4045 static void
4046 minimum_entry_lines_pref_cb(const char *name,
4047 PurplePrefType type,
4048 gconstpointer value,
4049 gpointer data)
4051 #if 0
4052 GList *l = purple_conversations_get_all();
4053 PurpleConversation *conv;
4054 while (l != NULL)
4056 conv = (PurpleConversation *)l->data;
4058 if (PIDGIN_IS_PIDGIN_CONVERSATION(conv))
4059 resize_webview_cb(PIDGIN_CONVERSATION(conv));
4060 l = l->next;
4062 #endif
4065 static void
4066 setup_chat_topic(PidginConversation *gtkconv, GtkWidget *vbox)
4068 PurpleConversation *conv = gtkconv->active_conv;
4069 PurpleConnection *gc = purple_conversation_get_connection(conv);
4070 PurpleProtocol *protocol = purple_connection_get_protocol(gc);
4071 if (purple_protocol_get_options(protocol) & OPT_PROTO_CHAT_TOPIC)
4073 GtkWidget *hbox, *label;
4074 PidginChatPane *gtkchat = gtkconv->u.chat;
4076 hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, PIDGIN_HIG_BOX_SPACE);
4077 gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0);
4079 label = gtk_label_new(_("Topic:"));
4080 gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 0);
4082 gtkchat->topic_text = gtk_entry_new();
4083 gtk_widget_set_size_request(gtkchat->topic_text, -1, BUDDYICON_SIZE_MIN);
4085 if(!PURPLE_PROTOCOL_IMPLEMENTS(protocol, CHAT, set_topic)) {
4086 gtk_editable_set_editable(GTK_EDITABLE(gtkchat->topic_text), FALSE);
4087 } else {
4088 g_signal_connect(G_OBJECT(gtkchat->topic_text), "activate",
4089 G_CALLBACK(topic_callback), gtkconv);
4092 gtk_box_pack_start(GTK_BOX(hbox), gtkchat->topic_text, TRUE, TRUE, 0);
4093 g_signal_connect(G_OBJECT(gtkchat->topic_text), "key_press_event",
4094 G_CALLBACK(entry_key_press_cb), gtkconv);
4098 static gboolean
4099 pidgin_conv_userlist_create_tooltip(GtkWidget *tipwindow, GtkTreePath *path,
4100 gpointer userdata, int *w, int *h)
4102 PidginConversation *gtkconv = userdata;
4103 GtkTreeIter iter;
4104 GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkconv->u.chat->list));
4105 PurpleConversation *conv = gtkconv->active_conv;
4106 PurpleBlistNode *node;
4107 PurpleProtocol *protocol;
4108 PurpleAccount *account = purple_conversation_get_account(conv);
4109 char *who = NULL;
4111 if (purple_account_get_connection(account) == NULL)
4112 return FALSE;
4114 if (!gtk_tree_model_get_iter(GTK_TREE_MODEL(model), &iter, path))
4115 return FALSE;
4117 gtk_tree_model_get(GTK_TREE_MODEL(model), &iter, CHAT_USERS_NAME_COLUMN, &who, -1);
4119 protocol = purple_connection_get_protocol(purple_account_get_connection(account));
4120 node = (PurpleBlistNode*)(purple_blist_find_buddy(purple_conversation_get_account(conv), who));
4121 if (node && protocol && (purple_protocol_get_options(protocol) & OPT_PROTO_UNIQUE_CHATNAME))
4122 pidgin_blist_draw_tooltip(node, gtkconv->infopane);
4124 g_free(who);
4125 return FALSE;
4128 static void
4129 setup_chat_userlist(PidginConversation *gtkconv, GtkWidget *hpaned)
4131 PidginChatPane *gtkchat = gtkconv->u.chat;
4132 GtkWidget *lbox, *list;
4133 GtkListStore *ls;
4134 GtkCellRenderer *rend;
4135 GtkTreeViewColumn *col;
4136 int ul_width;
4137 void *blist_handle = purple_blist_get_handle();
4138 PurpleConversation *conv = gtkconv->active_conv;
4140 /* Build the right pane. */
4141 lbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, PIDGIN_HIG_BOX_SPACE);
4142 gtk_paned_pack2(GTK_PANED(hpaned), lbox, FALSE, TRUE);
4143 gtk_widget_show(lbox);
4145 /* Setup the label telling how many people are in the room. */
4146 gtkchat->count = gtk_label_new(_("0 people in room"));
4147 gtk_label_set_ellipsize(GTK_LABEL(gtkchat->count), PANGO_ELLIPSIZE_END);
4148 gtk_box_pack_start(GTK_BOX(lbox), gtkchat->count, FALSE, FALSE, 0);
4149 gtk_widget_show(gtkchat->count);
4151 /* Setup the list of users. */
4153 ls = gtk_list_store_new(CHAT_USERS_COLUMNS, GDK_TYPE_PIXBUF, G_TYPE_STRING,
4154 G_TYPE_STRING, G_TYPE_STRING, G_TYPE_INT,
4155 GDK_TYPE_RGBA, G_TYPE_INT, G_TYPE_STRING);
4156 gtk_tree_sortable_set_sort_func(GTK_TREE_SORTABLE(ls), CHAT_USERS_ALIAS_KEY_COLUMN,
4157 sort_chat_users, NULL, NULL);
4159 list = gtk_tree_view_new_with_model(GTK_TREE_MODEL(ls));
4161 /* Allow a user to specify gtkrc settings for the chat userlist only */
4162 gtk_widget_set_name(list, "pidgin_conv_userlist");
4164 rend = gtk_cell_renderer_pixbuf_new();
4165 g_object_set(G_OBJECT(rend),
4166 "stock-size", gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_EXTRA_SMALL),
4167 NULL);
4168 col = gtk_tree_view_column_new_with_attributes(NULL, rend,
4169 "stock-id", CHAT_USERS_ICON_STOCK_COLUMN, NULL);
4170 gtk_tree_view_column_set_sizing(col, GTK_TREE_VIEW_COLUMN_AUTOSIZE);
4171 gtk_tree_view_append_column(GTK_TREE_VIEW(list), col);
4172 ul_width = purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/chat/userlist_width");
4173 gtk_widget_set_size_request(lbox, ul_width, -1);
4175 /* Hack to prevent completely collapsed userlist coming back with a 1 pixel width.
4176 * I would have liked to use the GtkPaned "max-position", but for some reason that didn't work */
4177 if (ul_width == 0)
4178 gtk_paned_set_position(GTK_PANED(hpaned), 999999);
4180 g_signal_connect(G_OBJECT(list), "button_press_event",
4181 G_CALLBACK(right_click_chat_cb), gtkconv);
4182 g_signal_connect(G_OBJECT(list), "row-activated",
4183 G_CALLBACK(activate_list_cb), gtkconv);
4184 g_signal_connect(G_OBJECT(list), "popup-menu",
4185 G_CALLBACK(gtkconv_chat_popup_menu_cb), gtkconv);
4186 g_signal_connect(G_OBJECT(lbox), "size-allocate", G_CALLBACK(lbox_size_allocate_cb), gtkconv);
4188 pidgin_tooltip_setup_for_treeview(list, gtkconv,
4189 pidgin_conv_userlist_create_tooltip, NULL);
4191 rend = gtk_cell_renderer_text_new();
4192 g_object_set(rend,
4193 "foreground-set", TRUE,
4194 "weight-set", TRUE,
4195 NULL);
4196 g_object_set(G_OBJECT(rend), "editable", TRUE, NULL);
4198 col = gtk_tree_view_column_new_with_attributes(NULL, rend,
4199 "text", CHAT_USERS_ALIAS_COLUMN,
4200 "foreground-rgba", CHAT_USERS_COLOR_COLUMN,
4201 "weight", CHAT_USERS_WEIGHT_COLUMN,
4202 NULL);
4204 purple_signal_connect(blist_handle, "blist-node-added",
4205 gtkchat, PURPLE_CALLBACK(buddy_added_cb), conv);
4206 purple_signal_connect(blist_handle, "blist-node-removed",
4207 gtkchat, PURPLE_CALLBACK(buddy_removed_cb), conv);
4208 purple_signal_connect(blist_handle, "blist-node-aliased",
4209 gtkchat, PURPLE_CALLBACK(blist_node_aliased_cb), conv);
4211 gtk_tree_view_column_set_expand(col, TRUE);
4212 g_object_set(rend, "ellipsize", PANGO_ELLIPSIZE_END, NULL);
4214 gtk_tree_view_append_column(GTK_TREE_VIEW(list), col);
4216 gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(list), FALSE);
4217 gtk_widget_show(list);
4219 gtkchat->list = list;
4221 gtk_box_pack_start(GTK_BOX(lbox),
4222 pidgin_make_scrollable(list, GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC, GTK_SHADOW_IN, -1, -1),
4223 TRUE, TRUE, 0);
4226 static gboolean
4227 pidgin_conv_create_tooltip(GtkWidget *tipwindow, gpointer userdata, int *w, int *h)
4229 PurpleBlistNode *node = NULL;
4230 PurpleConversation *conv;
4231 PidginConversation *gtkconv = userdata;
4233 conv = gtkconv->active_conv;
4234 if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
4235 node = (PurpleBlistNode*)(purple_blist_find_chat(purple_conversation_get_account(conv), purple_conversation_get_name(conv)));
4236 if (!node)
4237 node = g_object_get_data(G_OBJECT(gtkconv->history), "transient_chat");
4238 } else {
4239 node = (PurpleBlistNode*)(purple_blist_find_buddy(purple_conversation_get_account(conv), purple_conversation_get_name(conv)));
4240 #if 0
4241 /* Using the transient blist nodes to show the tooltip doesn't quite work yet. */
4242 if (!node)
4243 node = g_object_get_data(G_OBJECT(gtkconv->webview), "transient_buddy");
4244 #endif
4247 if (node)
4248 pidgin_blist_draw_tooltip(node, gtkconv->infopane);
4249 return FALSE;
4252 static GtkWidget *
4253 setup_common_pane(PidginConversation *gtkconv)
4255 GtkWidget *vbox, *sw, *event_box, *view;
4256 GtkCellRenderer *rend;
4257 GtkTreePath *path;
4258 PurpleConversation *conv = gtkconv->active_conv;
4259 PurpleBuddy *buddy;
4260 gboolean chat = PURPLE_IS_CHAT_CONVERSATION(conv);
4261 int buddyicon_size = 0;
4263 /* Setup the top part of the pane */
4264 vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, PIDGIN_HIG_BOX_SPACE);
4265 gtk_widget_show(vbox);
4267 /* Setup the info pane */
4268 event_box = gtk_event_box_new();
4269 gtk_event_box_set_visible_window(GTK_EVENT_BOX(event_box), FALSE);
4270 gtk_widget_show(event_box);
4271 gtkconv->infopane_hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
4272 gtk_box_pack_start(GTK_BOX(vbox), event_box, FALSE, FALSE, 0);
4273 gtk_container_add(GTK_CONTAINER(event_box), gtkconv->infopane_hbox);
4274 gtk_widget_show(gtkconv->infopane_hbox);
4275 gtk_widget_add_events(event_box,
4276 GDK_POINTER_MOTION_MASK | GDK_LEAVE_NOTIFY_MASK);
4277 g_signal_connect(G_OBJECT(event_box), "button-press-event",
4278 G_CALLBACK(infopane_press_cb), gtkconv);
4280 pidgin_tooltip_setup_for_widget(event_box, gtkconv,
4281 pidgin_conv_create_tooltip, NULL);
4283 gtkconv->infopane = gtk_cell_view_new();
4284 gtkconv->infopane_model = gtk_list_store_new(CONV_NUM_COLUMNS, G_TYPE_STRING, G_TYPE_STRING, GDK_TYPE_PIXBUF, GDK_TYPE_PIXBUF);
4285 gtk_cell_view_set_model(GTK_CELL_VIEW(gtkconv->infopane),
4286 GTK_TREE_MODEL(gtkconv->infopane_model));
4287 g_object_unref(gtkconv->infopane_model);
4288 gtk_list_store_append(gtkconv->infopane_model, &(gtkconv->infopane_iter));
4289 gtk_box_pack_start(GTK_BOX(gtkconv->infopane_hbox), gtkconv->infopane, TRUE, TRUE, 0);
4290 path = gtk_tree_path_new_from_string("0");
4291 gtk_cell_view_set_displayed_row(GTK_CELL_VIEW(gtkconv->infopane), path);
4292 gtk_tree_path_free(path);
4294 if (chat) {
4295 /* This empty widget is used to ensure that the infopane is consistently
4296 sized for chat windows. The correct fix is to put an icon in the chat
4297 window as well, because that would make "Set Custom Icon" consistent
4298 for both the buddy list and the chat window, but PidginConversation
4299 is pretty much stuck until 3.0. */
4300 GtkWidget *sizing_vbox;
4301 sizing_vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
4302 gtk_widget_set_size_request(sizing_vbox, -1, BUDDYICON_SIZE_MIN);
4303 gtk_box_pack_start(GTK_BOX(gtkconv->infopane_hbox), sizing_vbox, FALSE, FALSE, 0);
4304 gtk_widget_show(sizing_vbox);
4306 else {
4307 gtkconv->u.im->icon_container = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
4309 if ((buddy = purple_blist_find_buddy(purple_conversation_get_account(conv),
4310 purple_conversation_get_name(conv))) != NULL) {
4311 PurpleContact *contact = purple_buddy_get_contact(buddy);
4312 if (contact) {
4313 buddyicon_size = purple_blist_node_get_int((PurpleBlistNode*)contact, "pidgin-infopane-iconsize");
4316 buddyicon_size = CLAMP(buddyicon_size, BUDDYICON_SIZE_MIN, BUDDYICON_SIZE_MAX);
4317 gtk_widget_set_size_request(gtkconv->u.im->icon_container, -1, buddyicon_size);
4319 gtk_box_pack_start(GTK_BOX(gtkconv->infopane_hbox),
4320 gtkconv->u.im->icon_container, FALSE, FALSE, 0);
4322 gtk_widget_show(gtkconv->u.im->icon_container);
4325 gtk_widget_show(gtkconv->infopane);
4327 rend = gtk_cell_renderer_pixbuf_new();
4328 gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(gtkconv->infopane), rend, FALSE);
4329 gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(gtkconv->infopane), rend, "stock-id", CONV_ICON_COLUMN, NULL);
4330 g_object_set(rend, "xalign", 0.0, "xpad", 6, "ypad", 0,
4331 "stock-size", gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_EXTRA_SMALL),
4332 NULL);
4334 rend = gtk_cell_renderer_text_new();
4335 gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(gtkconv->infopane), rend, TRUE);
4336 gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(gtkconv->infopane), rend, "markup", CONV_TEXT_COLUMN, NULL);
4337 g_object_set(rend, "ypad", 0, "yalign", 0.5, NULL);
4339 g_object_set(rend, "ellipsize", PANGO_ELLIPSIZE_END, NULL);
4341 rend = gtk_cell_renderer_pixbuf_new();
4342 gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(gtkconv->infopane), rend, FALSE);
4343 gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(gtkconv->infopane), rend, "pixbuf", CONV_PROTOCOL_ICON_COLUMN, NULL);
4344 g_object_set(rend, "xalign", 0.0, "xpad", 3, "ypad", 0, NULL);
4346 rend = gtk_cell_renderer_pixbuf_new();
4347 gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(gtkconv->infopane), rend, FALSE);
4348 gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(gtkconv->infopane), rend, "pixbuf", CONV_EMBLEM_COLUMN, NULL);
4349 g_object_set(rend, "xalign", 0.0, "xpad", 6, "ypad", 0, NULL);
4351 /* Setup the history widget */
4352 sw = gtk_scrolled_window_new(NULL, NULL);
4353 gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(sw), GTK_SHADOW_IN);
4354 gtk_scrolled_window_set_policy(
4355 GTK_SCROLLED_WINDOW(sw),
4356 GTK_POLICY_NEVER,
4357 GTK_POLICY_ALWAYS
4360 gtkconv->history_buffer = talkatu_history_buffer_new();
4361 gtkconv->history = talkatu_history_new();
4362 gtk_text_view_set_buffer(GTK_TEXT_VIEW(gtkconv->history), gtkconv->history_buffer);
4363 gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(gtkconv->history), GTK_WRAP_WORD);
4364 gtk_container_add(GTK_CONTAINER(sw), gtkconv->history);
4366 if (chat) {
4367 GtkWidget *hpaned;
4369 /* Add the topic */
4370 setup_chat_topic(gtkconv, vbox);
4372 /* Add the talkatu history */
4373 hpaned = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL);
4374 gtk_box_pack_start(GTK_BOX(vbox), hpaned, TRUE, TRUE, 0);
4375 gtk_widget_show(hpaned);
4376 gtk_paned_pack1(GTK_PANED(hpaned), sw, TRUE, TRUE);
4378 /* Now add the userlist */
4379 setup_chat_userlist(gtkconv, hpaned);
4380 } else {
4381 gtk_box_pack_start(GTK_BOX(vbox), sw, TRUE, TRUE, 0);
4383 gtk_widget_show_all(sw);
4385 g_object_set_data(G_OBJECT(gtkconv->history), "gtkconv", gtkconv);
4387 g_signal_connect(G_OBJECT(gtkconv->history), "key_press_event",
4388 G_CALLBACK(refocus_entry_cb), gtkconv);
4389 g_signal_connect(G_OBJECT(gtkconv->history), "key_release_event",
4390 G_CALLBACK(refocus_entry_cb), gtkconv);
4392 gtkconv->lower_hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, PIDGIN_HIG_BOX_SPACE);
4393 gtk_box_pack_start(GTK_BOX(vbox), gtkconv->lower_hbox, FALSE, FALSE, 0);
4394 gtk_widget_show(gtkconv->lower_hbox);
4396 /* Setup the entry widget and all signals */
4397 gtkconv->editor = talkatu_editor_new();
4398 talkatu_editor_set_buffer(TALKATU_EDITOR(gtkconv->editor), talkatu_html_buffer_new());
4399 gtk_box_pack_start(GTK_BOX(gtkconv->lower_hbox), gtkconv->editor, TRUE, TRUE, 0);
4401 view = talkatu_editor_get_view(TALKATU_EDITOR(gtkconv->editor));
4402 gtk_widget_set_name(view, "pidgin_conv_entry");
4403 talkatu_view_set_send_binding(TALKATU_VIEW(view), TALKATU_VIEW_SEND_BINDING_RETURN | TALKATU_VIEW_SEND_BINDING_KP_ENTER);
4404 g_signal_connect(
4405 G_OBJECT(view),
4406 "send-message",
4407 G_CALLBACK(send_cb),
4408 gtkconv
4411 if (!chat) {
4412 /* For sending typing notifications for IMs */
4413 gtkconv->u.im->typing_timer = 0;
4414 gtkconv->u.im->animate = purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/im/animate_buddy_icons");
4415 gtkconv->u.im->show_icon = TRUE;
4418 return vbox;
4421 static PidginConversation *
4422 pidgin_conv_find_gtkconv(PurpleConversation * conv)
4424 PurpleBuddy *bud = purple_blist_find_buddy(purple_conversation_get_account(conv), purple_conversation_get_name(conv));
4425 PurpleContact *c;
4426 PurpleBlistNode *cn, *bn;
4428 if (!bud)
4429 return NULL;
4431 if (!(c = purple_buddy_get_contact(bud)))
4432 return NULL;
4434 cn = PURPLE_BLIST_NODE(c);
4435 for (bn = purple_blist_node_get_first_child(cn); bn; bn = purple_blist_node_get_sibling_next(bn)) {
4436 PurpleBuddy *b = PURPLE_BUDDY(bn);
4437 PurpleIMConversation *im;
4438 if ((im = purple_conversations_find_im_with_account(purple_buddy_get_name(b), purple_buddy_get_account(b)))) {
4439 if (PIDGIN_CONVERSATION(PURPLE_CONVERSATION(im)))
4440 return PIDGIN_CONVERSATION(PURPLE_CONVERSATION(im));
4444 return NULL;
4447 static void
4448 buddy_update_cb(PurpleBlistNode *bnode, gpointer null)
4450 GList *list;
4452 g_return_if_fail(bnode);
4453 if (!PURPLE_IS_BUDDY(bnode))
4454 return;
4456 for (list = pidgin_conv_windows_get_list(); list; list = list->next)
4458 PidginConvWindow *win = list->data;
4459 PurpleConversation *conv = pidgin_conv_window_get_active_conversation(win);
4461 if (!PURPLE_IS_IM_CONVERSATION(conv))
4462 continue;
4464 pidgin_conv_update_fields(conv, PIDGIN_CONV_MENU);
4468 static gboolean
4469 ignore_middle_click(GtkWidget *widget, GdkEventButton *e, gpointer null)
4471 /* A click on the pane is propagated to the notebook containing the pane.
4472 * So if Stu accidentally aims high and middle clicks on the pane-handle,
4473 * it causes a conversation tab to close. Let's stop that from happening.
4475 if (e->button == GDK_BUTTON_MIDDLE && e->type == GDK_BUTTON_PRESS)
4476 return TRUE;
4477 return FALSE;
4480 /**************************************************************************
4481 * Conversation UI operations
4482 **************************************************************************/
4483 static void
4484 private_gtkconv_new(PurpleConversation *conv, gboolean hidden)
4486 PidginConversation *gtkconv;
4487 GtkWidget *pane = NULL;
4488 GtkWidget *tab_cont;
4489 PurpleBlistNode *convnode;
4491 if (PURPLE_IS_IM_CONVERSATION(conv) && (gtkconv = pidgin_conv_find_gtkconv(conv))) {
4492 purple_conversation_set_ui_data(conv, gtkconv);
4493 if (!g_list_find(gtkconv->convs, conv))
4494 gtkconv->convs = g_list_prepend(gtkconv->convs, conv);
4495 pidgin_conv_switch_active_conversation(conv);
4496 return;
4499 gtkconv = g_new0(PidginConversation, 1);
4500 purple_conversation_set_ui_data(conv, gtkconv);
4501 gtkconv->active_conv = conv;
4502 gtkconv->convs = g_list_prepend(gtkconv->convs, conv);
4503 gtkconv->send_history = g_list_append(NULL, NULL);
4505 /* Setup some initial variables. */
4506 gtkconv->unseen_state = PIDGIN_UNSEEN_NONE;
4507 gtkconv->unseen_count = 0;
4508 gtkconv->last_flags = 0;
4510 if (PURPLE_IS_IM_CONVERSATION(conv)) {
4511 gtkconv->u.im = g_malloc0(sizeof(PidginImPane));
4512 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
4513 gtkconv->u.chat = g_malloc0(sizeof(PidginChatPane));
4515 pane = setup_common_pane(gtkconv);
4517 if (pane == NULL) {
4518 if (PURPLE_IS_CHAT_CONVERSATION(conv))
4519 g_free(gtkconv->u.chat);
4520 else if (PURPLE_IS_IM_CONVERSATION(conv))
4521 g_free(gtkconv->u.im);
4523 g_free(gtkconv);
4524 purple_conversation_set_ui_data(conv, NULL);
4525 return;
4528 g_signal_connect(G_OBJECT(pane), "button_press_event",
4529 G_CALLBACK(ignore_middle_click), NULL);
4531 /* Setup the container for the tab. */
4532 gtkconv->tab_cont = tab_cont = gtk_box_new(GTK_ORIENTATION_VERTICAL, PIDGIN_HIG_BOX_SPACE);
4533 g_object_set_data(G_OBJECT(tab_cont), "PidginConversation", gtkconv);
4534 gtk_container_set_border_width(GTK_CONTAINER(tab_cont), PIDGIN_HIG_BOX_SPACE);
4535 gtk_box_pack_start(GTK_BOX(tab_cont), pane, TRUE, TRUE, 0);
4536 gtk_widget_show(pane);
4538 convnode = get_conversation_blist_node(conv);
4539 if (convnode == NULL || !purple_blist_node_get_bool(convnode, "gtk-mute-sound"))
4540 gtkconv->make_sound = TRUE;
4542 if (convnode != NULL && purple_blist_node_has_setting(convnode, "enable-logging")) {
4543 gboolean logging = purple_blist_node_get_bool(convnode, "enable-logging");
4544 purple_conversation_set_logging(conv, logging);
4547 talkatu_editor_set_toolbar_visible(
4548 TALKATU_EDITOR(gtkconv->editor),
4549 purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/show_formatting_toolbar")
4552 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/im/show_buddy_icons"))
4553 gtk_widget_show(gtkconv->infopane_hbox);
4554 else
4555 gtk_widget_hide(gtkconv->infopane_hbox);
4558 g_signal_connect_swapped(G_OBJECT(pane), "focus",
4559 G_CALLBACK(gtk_widget_grab_focus),
4560 gtkconv->editor);
4562 if (hidden)
4563 pidgin_conv_window_add_gtkconv(hidden_convwin, gtkconv);
4564 else
4565 pidgin_conv_placement_place(gtkconv);
4567 if (generated_nick_colors == NULL) {
4568 GdkColor color;
4569 GdkRGBA rgba;
4570 color = gtk_widget_get_style(gtkconv->history)->base[GTK_STATE_NORMAL];
4571 rgba.red = color.red / 65535.0;
4572 rgba.green = color.green / 65535.0;
4573 rgba.blue = color.blue / 65535.0;
4574 rgba.alpha = 1.0;
4575 generated_nick_colors = generate_nick_colors(NICK_COLOR_GENERATE_COUNT, rgba);
4578 gtkconv->nick_colors = g_array_ref(generated_nick_colors);
4581 static void
4582 pidgin_conv_new_hidden(PurpleConversation *conv)
4584 private_gtkconv_new(conv, TRUE);
4587 void
4588 pidgin_conv_new(PurpleConversation *conv)
4590 private_gtkconv_new(conv, FALSE);
4591 if (PIDGIN_IS_PIDGIN_CONVERSATION(conv))
4592 purple_signal_emit(pidgin_conversations_get_handle(),
4593 "conversation-displayed", PIDGIN_CONVERSATION(conv));
4596 static void
4597 received_im_msg_cb(PurpleAccount *account, char *sender, char *message,
4598 PurpleConversation *conv, PurpleMessageFlags flags)
4600 PurpleConversationUiOps *ui_ops = pidgin_conversations_get_conv_ui_ops();
4601 gboolean hide = FALSE;
4602 guint timer;
4604 /* create hidden conv if hide_new pref is always */
4605 if (purple_strequal(purple_prefs_get_string(PIDGIN_PREFS_ROOT "/conversations/im/hide_new"), "always"))
4606 hide = TRUE;
4608 /* create hidden conv if hide_new pref is away and account is away */
4609 if (purple_strequal(purple_prefs_get_string(PIDGIN_PREFS_ROOT "/conversations/im/hide_new"), "away") &&
4610 !purple_status_is_available(purple_account_get_active_status(account)))
4611 hide = TRUE;
4613 if (conv && PIDGIN_IS_PIDGIN_CONVERSATION(conv) && !hide) {
4614 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
4615 if (gtkconv->win == hidden_convwin) {
4616 pidgin_conv_attach_to_conversation(gtkconv->active_conv);
4618 return;
4621 if (hide) {
4622 ui_ops->create_conversation = pidgin_conv_new_hidden;
4623 purple_im_conversation_new(account, sender);
4624 ui_ops->create_conversation = pidgin_conv_new;
4627 /* Somebody wants to keep this conversation around, so don't time it out */
4628 if (conv) {
4629 timer = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(conv), "close-timer"));
4630 if (timer) {
4631 g_source_remove(timer);
4632 g_object_set_data(G_OBJECT(conv), "close-timer", GINT_TO_POINTER(0));
4637 static void
4638 pidgin_conv_destroy(PurpleConversation *conv)
4640 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
4642 gtkconv->convs = g_list_remove(gtkconv->convs, conv);
4643 /* Don't destroy ourselves until all our convos are gone */
4644 if (gtkconv->convs) {
4645 /* Make sure the destroyed conversation is not the active one */
4646 if (gtkconv->active_conv == conv) {
4647 gtkconv->active_conv = gtkconv->convs->data;
4648 purple_conversation_update(gtkconv->active_conv, PURPLE_CONVERSATION_UPDATE_FEATURES);
4650 return;
4653 pidgin_conv_window_remove_gtkconv(gtkconv->win, gtkconv);
4655 /* If the "Save Conversation" or "Save Icon" dialogs are open then close them */
4656 purple_request_close_with_handle(gtkconv);
4657 purple_notify_close_with_handle(gtkconv);
4659 gtk_widget_destroy(gtkconv->tab_cont);
4660 g_object_unref(gtkconv->tab_cont);
4662 if (PURPLE_IS_IM_CONVERSATION(conv)) {
4663 if (gtkconv->u.im->icon_timer != 0)
4664 g_source_remove(gtkconv->u.im->icon_timer);
4666 if (gtkconv->u.im->anim != NULL)
4667 g_object_unref(G_OBJECT(gtkconv->u.im->anim));
4669 if (gtkconv->u.im->typing_timer != 0)
4670 g_source_remove(gtkconv->u.im->typing_timer);
4672 g_free(gtkconv->u.im);
4673 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
4674 purple_signals_disconnect_by_handle(gtkconv->u.chat);
4675 g_free(gtkconv->u.chat);
4678 gtkconv->send_history = g_list_first(gtkconv->send_history);
4679 g_list_foreach(gtkconv->send_history, (GFunc)g_free, NULL);
4680 g_list_free(gtkconv->send_history);
4682 if (gtkconv->attach_timer) {
4683 g_source_remove(gtkconv->attach_timer);
4686 g_array_unref(gtkconv->nick_colors);
4688 g_free(gtkconv);
4691 #if 0
4692 static const char *
4693 get_text_tag_color(GtkTextTag *tag)
4695 GdkRGBA *color = NULL;
4696 gboolean set = FALSE;
4697 static char colcode[] = "#XXXXXX";
4698 if (tag)
4699 g_object_get(G_OBJECT(tag), "foreground-set", &set, "foreground-rgba", &color, NULL);
4700 if (set && color)
4701 g_snprintf(colcode, sizeof(colcode), "#%02x%02x%02x",
4702 (unsigned int)(color->red * 255),
4703 (unsigned int)(color->green * 255),
4704 (unsigned int)(color->blue * 255));
4705 else
4706 colcode[0] = '\0';
4707 if (color)
4708 gdk_rgba_free(color);
4709 return colcode;
4712 /* The callback for an event on a link tag. */
4713 static gboolean buddytag_event(GtkTextTag *tag, GObject *imhtml,
4714 GdkEvent *event, GtkTextIter *arg2, gpointer data)
4716 if (event->type == GDK_BUTTON_PRESS
4717 || event->type == GDK_2BUTTON_PRESS) {
4718 GdkEventButton *btn_event = (GdkEventButton*) event;
4719 PurpleConversation *conv = data;
4720 char *buddyname;
4721 gchar *name;
4723 g_object_get(G_OBJECT(tag), "name", &name, NULL);
4725 /* strlen("BUDDY " or "HILIT ") == 6 */
4726 g_return_val_if_fail((name != NULL) && (strlen(name) > 6), FALSE);
4728 buddyname = name + 6;
4730 /* emit chat-nick-clicked signal */
4731 if (event->type == GDK_BUTTON_PRESS) {
4732 gint plugin_return = GPOINTER_TO_INT(purple_signal_emit_return_1(
4733 pidgin_conversations_get_handle(), "chat-nick-clicked",
4734 data, buddyname, btn_event->button));
4735 if (plugin_return) {
4736 g_free(name);
4737 return TRUE;
4741 if (btn_event->button == GDK_BUTTON_PRIMARY && event->type == GDK_2BUTTON_PRESS) {
4742 chat_do_im(PIDGIN_CONVERSATION(conv), buddyname);
4743 g_free(name);
4745 return TRUE;
4746 } else if (btn_event->button == GDK_BUTTON_MIDDLE && event->type == GDK_2BUTTON_PRESS) {
4747 chat_do_info(PIDGIN_CONVERSATION(conv), buddyname);
4748 g_free(name);
4750 return TRUE;
4751 } else if (gdk_event_triggers_context_menu(event)) {
4752 GtkTextIter start, end;
4754 /* we shouldn't display the popup
4755 * if the user has selected something: */
4756 if (!gtk_text_buffer_get_selection_bounds(
4757 gtk_text_iter_get_buffer(arg2),
4758 &start, &end)) {
4759 GtkWidget *menu = NULL;
4760 PurpleConnection *gc =
4761 purple_conversation_get_connection(conv);
4763 menu = create_chat_menu(conv, buddyname, gc);
4764 gtk_menu_popup_at_pointer(GTK_MENU(menu), event);
4766 g_free(name);
4768 /* Don't propagate the event any further */
4769 return TRUE;
4773 g_free(name);
4776 return FALSE;
4778 #endif
4780 static GtkTextTag *get_buddy_tag(PurpleChatConversation *chat, const char *who, PurpleMessageFlags flag,
4781 gboolean create)
4783 /* TODO WEBKIT */
4784 #if 0
4785 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
4786 GtkTextTag *buddytag;
4787 gchar *str;
4788 gboolean highlight = (flag & PURPLE_MESSAGE_NICK);
4789 GtkTextBuffer *buffer = GTK_IMHTML(gtkconv->imhtml)->text_buffer;
4791 str = g_strdup_printf(highlight ? "HILIT %s" : "BUDDY %s", who);
4793 buddytag = gtk_text_tag_table_lookup(
4794 gtk_text_buffer_get_tag_table(buffer), str);
4796 if (buddytag == NULL && create) {
4797 if (highlight)
4798 buddytag = gtk_text_buffer_create_tag(buffer, str,
4799 "foreground", get_text_tag_color(gtk_text_tag_table_lookup(
4800 gtk_text_buffer_get_tag_table(buffer), "highlight-name")),
4801 "weight", PANGO_WEIGHT_BOLD,
4802 NULL);
4803 else
4804 buddytag = gtk_text_buffer_create_tag(
4805 buffer, str,
4806 "foreground-rgba", get_nick_color(gtkconv, who),
4807 "weight", purple_blist_find_buddy(purple_conversation_get_account(conv), who) ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL,
4808 NULL);
4810 g_object_set_data(G_OBJECT(buddytag), "cursor", "");
4811 g_signal_connect(G_OBJECT(buddytag), "event",
4812 G_CALLBACK(buddytag_event), conv);
4815 g_free(str);
4817 return buddytag;
4818 #endif /* if 0 */
4819 return NULL;
4822 static gboolean
4823 writing_msg(PurpleConversation *conv, PurpleMessage *msg, gpointer _unused)
4825 PidginConversation *gtkconv;
4827 g_return_val_if_fail(msg != NULL, FALSE);
4829 if (!(purple_message_get_flags(msg) & PURPLE_MESSAGE_ACTIVE_ONLY))
4830 return FALSE;
4832 g_return_val_if_fail(conv != NULL, FALSE);
4833 gtkconv = PIDGIN_CONVERSATION(conv);
4834 g_return_val_if_fail(gtkconv != NULL, FALSE);
4836 if (conv == gtkconv->active_conv)
4837 return FALSE;
4839 purple_debug_info("gtkconv",
4840 "Suppressing message for an inactive conversation");
4842 return TRUE;
4845 static void
4846 pidgin_conv_write_conv(PurpleConversation *conv, PurpleMessage *pmsg)
4848 PidginMessage *pidgin_msg = NULL;
4849 PurpleMessageFlags flags;
4850 PidginConversation *gtkconv;
4851 PurpleConnection *gc;
4852 PurpleAccount *account;
4853 gboolean plugin_return;
4855 g_return_if_fail(conv != NULL);
4856 gtkconv = PIDGIN_CONVERSATION(conv);
4857 g_return_if_fail(gtkconv != NULL);
4858 flags = purple_message_get_flags(pmsg);
4860 #if 0
4861 if (gtkconv->attach_timer) {
4862 /* We are currently in the process of filling up the buffer with the message
4863 * history of the conversation. So we do not need to add the message here.
4864 * Instead, this message will be added to the message-list, which in turn will
4865 * be processed and displayed by the attach-callback.
4867 return;
4870 if (conv != gtkconv->active_conv)
4872 /* Set the active conversation to the one that just messaged us. */
4873 /* TODO: consider not doing this if the account is offline or something */
4874 if (flags & (PURPLE_MESSAGE_SEND | PURPLE_MESSAGE_RECV))
4875 pidgin_conv_switch_active_conversation(conv);
4877 #endif
4879 account = purple_conversation_get_account(conv);
4880 g_return_if_fail(account != NULL);
4881 gc = purple_account_get_connection(account);
4882 g_return_if_fail(gc != NULL || !(flags & (PURPLE_MESSAGE_SEND | PURPLE_MESSAGE_RECV)));
4884 plugin_return = GPOINTER_TO_INT(purple_signal_emit_return_1(
4885 pidgin_conversations_get_handle(),
4886 (PURPLE_IS_IM_CONVERSATION(conv) ? "displaying-im-msg" : "displaying-chat-msg"),
4887 conv, pmsg));
4888 if (plugin_return)
4890 return;
4893 pidgin_msg = pidgin_message_new(pmsg);
4894 talkatu_history_buffer_write_message(
4895 TALKATU_HISTORY_BUFFER(gtkconv->history_buffer),
4896 TALKATU_MESSAGE(pidgin_msg)
4899 /* Tab highlighting stuff */
4900 if (!(flags & PURPLE_MESSAGE_SEND) && !pidgin_conv_has_focus(conv))
4902 PidginUnseenState unseen = PIDGIN_UNSEEN_NONE;
4904 if ((flags & PURPLE_MESSAGE_NICK) == PURPLE_MESSAGE_NICK)
4905 unseen = PIDGIN_UNSEEN_NICK;
4906 else if (((flags & PURPLE_MESSAGE_SYSTEM) == PURPLE_MESSAGE_SYSTEM) ||
4907 ((flags & PURPLE_MESSAGE_ERROR) == PURPLE_MESSAGE_ERROR))
4908 unseen = PIDGIN_UNSEEN_EVENT;
4909 else if ((flags & PURPLE_MESSAGE_NO_LOG) == PURPLE_MESSAGE_NO_LOG)
4910 unseen = PIDGIN_UNSEEN_NO_LOG;
4911 else
4912 unseen = PIDGIN_UNSEEN_TEXT;
4914 gtkconv_set_unseen(gtkconv, unseen);
4917 /* on rejoin only request message history from after this message */
4918 if (flags & (PURPLE_MESSAGE_SEND | PURPLE_MESSAGE_RECV) &&
4919 PURPLE_IS_CHAT_CONVERSATION(conv)) {
4920 PurpleChat *chat = purple_blist_find_chat(
4921 purple_conversation_get_account(conv),
4922 purple_conversation_get_name(conv));
4923 if (chat) {
4924 GHashTable *comps = purple_chat_get_components(chat);
4925 time_t now, history_since, prev_history_since = 0;
4926 struct tm *history_since_tm;
4927 const char *history_since_s, *prev_history_since_s;
4929 history_since = purple_message_get_time(pmsg) + 1;
4931 prev_history_since_s = g_hash_table_lookup(comps,
4932 "history_since");
4933 if (prev_history_since_s != NULL)
4934 prev_history_since = purple_str_to_time(
4935 prev_history_since_s, TRUE, NULL, NULL,
4936 NULL);
4938 now = time(NULL);
4939 /* in case of incorrectly stored timestamps */
4940 if (prev_history_since > now)
4941 prev_history_since = now;
4942 /* in case of delayed messages */
4943 if (history_since < prev_history_since)
4944 history_since = prev_history_since;
4946 history_since_tm = gmtime(&history_since);
4947 history_since_s = purple_utf8_strftime(
4948 "%Y-%m-%dT%H:%M:%SZ", history_since_tm);
4949 if (!purple_strequal(prev_history_since_s,
4950 history_since_s))
4951 g_hash_table_replace(comps,
4952 g_strdup("history_since"),
4953 g_strdup(history_since_s));
4957 purple_signal_emit(pidgin_conversations_get_handle(),
4958 (PURPLE_IS_IM_CONVERSATION(conv) ? "displayed-im-msg" : "displayed-chat-msg"),
4959 conv, pmsg);
4960 update_typing_message(gtkconv, NULL);
4963 static gboolean get_iter_from_chatuser(PurpleChatUser *cb, GtkTreeIter *iter)
4965 GtkTreeRowReference *ref;
4966 GtkTreePath *path;
4967 GtkTreeModel *model;
4969 g_return_val_if_fail(cb != NULL, FALSE);
4971 ref = purple_chat_user_get_ui_data(cb);
4972 if (!ref)
4973 return FALSE;
4975 if ((path = gtk_tree_row_reference_get_path(ref)) == NULL)
4976 return FALSE;
4978 model = gtk_tree_row_reference_get_model(ref);
4979 if (!gtk_tree_model_get_iter(GTK_TREE_MODEL(model), iter, path)) {
4980 gtk_tree_path_free(path);
4981 return FALSE;
4984 gtk_tree_path_free(path);
4985 return TRUE;
4988 static void
4989 pidgin_conv_chat_add_users(PurpleChatConversation *chat, GList *cbuddies, gboolean new_arrivals)
4991 PidginConversation *gtkconv;
4992 PidginChatPane *gtkchat;
4993 GtkListStore *ls;
4994 GList *l;
4996 char tmp[BUF_LONG];
4997 int num_users;
4999 gtkconv = PIDGIN_CONVERSATION(PURPLE_CONVERSATION(chat));
5000 gtkchat = gtkconv->u.chat;
5002 num_users = purple_chat_conversation_get_users_count(chat);
5004 g_snprintf(tmp, sizeof(tmp),
5005 ngettext("%d person in room", "%d people in room",
5006 num_users),
5007 num_users);
5009 gtk_label_set_text(GTK_LABEL(gtkchat->count), tmp);
5011 ls = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list)));
5013 gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(ls), GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID,
5014 GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID);
5016 l = cbuddies;
5017 while (l != NULL) {
5018 add_chat_user_common(chat, (PurpleChatUser *)l->data, NULL);
5019 l = l->next;
5022 /* Currently GTK+ maintains our sorted list after it's in the tree.
5023 * This may change if it turns out we can manage it faster ourselves.
5025 gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(ls), CHAT_USERS_ALIAS_KEY_COLUMN,
5026 GTK_SORT_ASCENDING);
5029 static void
5030 pidgin_conv_chat_rename_user(PurpleChatConversation *chat, const char *old_name,
5031 const char *new_name, const char *new_alias)
5033 PidginConversation *gtkconv;
5034 PidginChatPane *gtkchat;
5035 PurpleChatUser *old_chatuser, *new_chatuser;
5036 GtkTreeIter iter;
5037 GtkTreeModel *model;
5038 GtkTextTag *tag;
5040 gtkconv = PIDGIN_CONVERSATION(PURPLE_CONVERSATION(chat));
5041 gtkchat = gtkconv->u.chat;
5043 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
5045 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &iter))
5046 return;
5048 if ((tag = get_buddy_tag(chat, old_name, 0, FALSE)))
5049 g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_ITALIC, NULL);
5050 if ((tag = get_buddy_tag(chat, old_name, PURPLE_MESSAGE_NICK, FALSE)))
5051 g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_ITALIC, NULL);
5053 old_chatuser = purple_chat_conversation_find_user(chat, old_name);
5054 if (!old_chatuser)
5055 return;
5057 if (get_iter_from_chatuser(old_chatuser, &iter)) {
5058 GtkTreeRowReference *ref = purple_chat_user_get_ui_data(old_chatuser);
5060 gtk_list_store_remove(GTK_LIST_STORE(model), &iter);
5061 gtk_tree_row_reference_free(ref);
5062 purple_chat_user_set_ui_data(old_chatuser, NULL);
5065 g_return_if_fail(new_alias != NULL);
5067 new_chatuser = purple_chat_conversation_find_user(chat, new_name);
5069 add_chat_user_common(chat, new_chatuser, old_name);
5072 static void
5073 pidgin_conv_chat_remove_users(PurpleChatConversation *chat, GList *users)
5075 PidginConversation *gtkconv;
5076 PidginChatPane *gtkchat;
5077 GtkTreeIter iter;
5078 GtkTreeModel *model;
5079 GList *l;
5080 char tmp[BUF_LONG];
5081 int num_users;
5082 gboolean f;
5083 GtkTextTag *tag;
5085 gtkconv = PIDGIN_CONVERSATION(PURPLE_CONVERSATION(chat));
5086 gtkchat = gtkconv->u.chat;
5088 num_users = purple_chat_conversation_get_users_count(chat);
5090 for (l = users; l != NULL; l = l->next) {
5091 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
5093 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &iter))
5094 /* XXX: Break? */
5095 continue;
5097 do {
5098 char *val;
5100 gtk_tree_model_get(GTK_TREE_MODEL(model), &iter,
5101 CHAT_USERS_NAME_COLUMN, &val, -1);
5103 if (!purple_utf8_strcasecmp((char *)l->data, val)) {
5104 f = gtk_list_store_remove(GTK_LIST_STORE(model), &iter);
5106 else
5107 f = gtk_tree_model_iter_next(GTK_TREE_MODEL(model), &iter);
5109 g_free(val);
5110 } while (f);
5112 if ((tag = get_buddy_tag(chat, l->data, 0, FALSE)))
5113 g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_ITALIC, NULL);
5114 if ((tag = get_buddy_tag(chat, l->data, PURPLE_MESSAGE_NICK, FALSE)))
5115 g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_ITALIC, NULL);
5118 g_snprintf(tmp, sizeof(tmp),
5119 ngettext("%d person in room", "%d people in room",
5120 num_users), num_users);
5122 gtk_label_set_text(GTK_LABEL(gtkchat->count), tmp);
5125 static void
5126 pidgin_conv_chat_update_user(PurpleChatUser *chatuser)
5128 PurpleChatConversation *chat;
5129 PidginConversation *gtkconv;
5130 PidginChatPane *gtkchat;
5131 GtkTreeIter iter;
5132 GtkTreeModel *model;
5134 if (!chatuser)
5135 return;
5137 chat = purple_chat_user_get_chat(chatuser);
5138 gtkconv = PIDGIN_CONVERSATION(PURPLE_CONVERSATION(chat));
5139 gtkchat = gtkconv->u.chat;
5141 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
5143 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &iter))
5144 return;
5146 if (get_iter_from_chatuser(chatuser, &iter)) {
5147 GtkTreeRowReference *ref = purple_chat_user_get_ui_data(chatuser);
5148 gtk_list_store_remove(GTK_LIST_STORE(model), &iter);
5149 gtk_tree_row_reference_free(ref);
5150 purple_chat_user_set_ui_data(chatuser, NULL);
5153 if (chatuser)
5154 add_chat_user_common(chat, chatuser, NULL);
5157 gboolean
5158 pidgin_conv_has_focus(PurpleConversation *conv)
5160 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
5161 PidginConvWindow *win;
5162 gboolean has_focus;
5164 win = gtkconv->win;
5166 g_object_get(G_OBJECT(win->window), "has-toplevel-focus", &has_focus, NULL);
5168 if (has_focus && pidgin_conv_window_is_active_conversation(conv))
5169 return TRUE;
5171 return FALSE;
5175 * Makes sure all the menu items and all the buttons are hidden/shown and
5176 * sensitive/insensitive. This is called after changing tabs and when an
5177 * account signs on or off.
5179 static void
5180 gray_stuff_out(PidginConversation *gtkconv)
5182 PidginConvWindow *win;
5183 PurpleConversation *conv = gtkconv->active_conv;
5184 PurpleConnection *gc;
5185 PurpleProtocol *protocol = NULL;
5186 GdkPixbuf *window_icon = NULL;
5187 // PidginWebViewButtons buttons;
5188 PurpleAccount *account;
5190 win = pidgin_conv_get_window(gtkconv);
5191 gc = purple_conversation_get_connection(conv);
5192 account = purple_conversation_get_account(conv);
5194 if (gc != NULL)
5195 protocol = purple_connection_get_protocol(gc);
5197 if (win->menu->send_to != NULL)
5198 update_send_to_selection(win);
5201 * Handle hiding and showing stuff based on what type of conv this is.
5202 * Stuff that Purple IMs support in general should be shown for IM
5203 * conversations. Stuff that Purple chats support in general should be
5204 * shown for chat conversations. It doesn't matter whether the protocol
5205 * supports it or not--that only affects if the button or menu item
5206 * is sensitive or not.
5208 if (PURPLE_IS_IM_CONVERSATION(conv)) {
5209 /* Show stuff that applies to IMs, hide stuff that applies to chats */
5211 /* Deal with menu items */
5212 gtk_action_set_visible(win->menu->view_log, TRUE);
5213 gtk_action_set_visible(win->menu->send_file, TRUE);
5214 gtk_action_set_visible(win->menu->get_attention, TRUE);
5215 gtk_action_set_visible(win->menu->add_pounce, TRUE);
5216 gtk_action_set_visible(win->menu->get_info, TRUE);
5217 gtk_action_set_visible(win->menu->invite, FALSE);
5218 gtk_action_set_visible(win->menu->alias, TRUE);
5219 if (purple_account_privacy_check(account, purple_conversation_get_name(conv))) {
5220 gtk_action_set_visible(win->menu->unblock, FALSE);
5221 gtk_action_set_visible(win->menu->block, TRUE);
5222 } else {
5223 gtk_action_set_visible(win->menu->block, FALSE);
5224 gtk_action_set_visible(win->menu->unblock, TRUE);
5227 if (purple_blist_find_buddy(account, purple_conversation_get_name(conv)) == NULL) {
5228 gtk_action_set_visible(win->menu->add, TRUE);
5229 gtk_action_set_visible(win->menu->remove, FALSE);
5230 } else {
5231 gtk_action_set_visible(win->menu->remove, TRUE);
5232 gtk_action_set_visible(win->menu->add, FALSE);
5235 gtk_action_set_visible(win->menu->insert_link, TRUE);
5236 gtk_action_set_visible(win->menu->insert_image, TRUE);
5237 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
5238 /* Show stuff that applies to Chats, hide stuff that applies to IMs */
5240 /* Deal with menu items */
5241 gtk_action_set_visible(win->menu->view_log, TRUE);
5242 gtk_action_set_visible(win->menu->send_file, FALSE);
5243 gtk_action_set_visible(win->menu->get_attention, FALSE);
5244 gtk_action_set_visible(win->menu->add_pounce, FALSE);
5245 gtk_action_set_visible(win->menu->get_info, FALSE);
5246 gtk_action_set_visible(win->menu->invite, TRUE);
5247 gtk_action_set_visible(win->menu->alias, TRUE);
5248 gtk_action_set_visible(win->menu->block, FALSE);
5249 gtk_action_set_visible(win->menu->unblock, FALSE);
5251 if ((account == NULL) || purple_blist_find_chat(account, purple_conversation_get_name(conv)) == NULL) {
5252 /* If the chat is NOT in the buddy list */
5253 gtk_action_set_visible(win->menu->add, TRUE);
5254 gtk_action_set_visible(win->menu->remove, FALSE);
5255 } else {
5256 /* If the chat IS in the buddy list */
5257 gtk_action_set_visible(win->menu->add, FALSE);
5258 gtk_action_set_visible(win->menu->remove, TRUE);
5261 gtk_action_set_visible(win->menu->insert_link, TRUE);
5262 gtk_action_set_visible(win->menu->insert_image, TRUE);
5266 * Handle graying stuff out based on whether an account is connected
5267 * and what features that account supports.
5269 if ((gc != NULL) &&
5270 (!PURPLE_IS_CHAT_CONVERSATION(conv) ||
5271 !purple_chat_conversation_has_left(PURPLE_CHAT_CONVERSATION(conv)) ))
5273 PurpleConnectionFlags features = purple_conversation_get_features(conv);
5274 /* Account is online */
5275 /* Deal with the toolbar */
5276 #if 0
5277 if (features & PURPLE_CONNECTION_FLAG_HTML)
5279 buttons = PIDGIN_WEBVIEW_ALL; /* Everything on */
5280 if (features & PURPLE_CONNECTION_FLAG_NO_BGCOLOR)
5281 buttons &= ~PIDGIN_WEBVIEW_BACKCOLOR;
5282 if (features & PURPLE_CONNECTION_FLAG_NO_FONTSIZE)
5284 buttons &= ~PIDGIN_WEBVIEW_GROW;
5285 buttons &= ~PIDGIN_WEBVIEW_SHRINK;
5287 if (features & PURPLE_CONNECTION_FLAG_NO_URLDESC)
5288 buttons &= ~PIDGIN_WEBVIEW_LINKDESC
5289 } else {
5290 buttons = PIDGIN_WEBVIEW_SMILEY | PIDGIN_WEBVIEW_IMAGE;
5293 if (features & PURPLE_CONNECTION_FLAG_NO_IMAGES)
5294 buttons &= ~PIDGIN_WEBVIEW_IMAGE;
5296 if (features & PURPLE_CONNECTION_FLAG_ALLOW_CUSTOM_SMILEY)
5297 buttons |= PIDGIN_WEBVIEW_CUSTOM_SMILEY;
5298 else
5299 buttons &= ~PIDGIN_WEBVIEW_CUSTOM_SMILEY;
5301 pidgin_webview_set_format_functions(PIDGIN_WEBVIEW(gtkconv->entry), buttons);
5302 #endif
5304 /* Deal with menu items */
5305 gtk_action_set_sensitive(win->menu->view_log, TRUE);
5306 gtk_action_set_sensitive(win->menu->add_pounce, TRUE);
5307 gtk_action_set_sensitive(win->menu->get_info, (PURPLE_PROTOCOL_IMPLEMENTS(protocol, SERVER, get_info)));
5308 gtk_action_set_sensitive(win->menu->invite, (PURPLE_PROTOCOL_IMPLEMENTS(protocol, CHAT, invite)));
5309 gtk_action_set_sensitive(win->menu->insert_link, (features & PURPLE_CONNECTION_FLAG_HTML));
5310 gtk_action_set_sensitive(win->menu->insert_image, !(features & PURPLE_CONNECTION_FLAG_NO_IMAGES));
5312 if (PURPLE_IS_IM_CONVERSATION(conv))
5314 gboolean can_send_file = FALSE;
5315 const gchar *name = purple_conversation_get_name(conv);
5317 if (PURPLE_IS_PROTOCOL_XFER(protocol) &&
5318 purple_protocol_xfer_can_receive(PURPLE_PROTOCOL_XFER(protocol), gc, name)
5320 can_send_file = TRUE;
5323 gtk_action_set_sensitive(win->menu->add, (PURPLE_PROTOCOL_IMPLEMENTS(protocol, SERVER, add_buddy)));
5324 gtk_action_set_sensitive(win->menu->remove, (PURPLE_PROTOCOL_IMPLEMENTS(protocol, SERVER, remove_buddy)));
5325 gtk_action_set_sensitive(win->menu->send_file, can_send_file);
5326 gtk_action_set_sensitive(win->menu->get_attention, (PURPLE_IS_PROTOCOL_ATTENTION(protocol)));
5327 gtk_action_set_sensitive(win->menu->alias,
5328 (account != NULL) &&
5329 (purple_blist_find_buddy(account, purple_conversation_get_name(conv)) != NULL));
5331 else if (PURPLE_IS_CHAT_CONVERSATION(conv))
5333 gtk_action_set_sensitive(win->menu->add, (PURPLE_PROTOCOL_IMPLEMENTS(protocol, CHAT, join)));
5334 gtk_action_set_sensitive(win->menu->remove, (PURPLE_PROTOCOL_IMPLEMENTS(protocol, CHAT, join)));
5335 gtk_action_set_sensitive(win->menu->alias,
5336 (account != NULL) &&
5337 (purple_blist_find_chat(account, purple_conversation_get_name(conv)) != NULL));
5340 } else {
5341 /* Account is offline */
5342 /* Or it's a chat that we've left. */
5344 /* Then deal with menu items */
5345 gtk_action_set_sensitive(win->menu->view_log, TRUE);
5346 gtk_action_set_sensitive(win->menu->send_file, FALSE);
5347 gtk_action_set_sensitive(win->menu->get_attention, FALSE);
5348 gtk_action_set_sensitive(win->menu->add_pounce, TRUE);
5349 gtk_action_set_sensitive(win->menu->get_info, FALSE);
5350 gtk_action_set_sensitive(win->menu->invite, FALSE);
5351 gtk_action_set_sensitive(win->menu->alias, FALSE);
5352 gtk_action_set_sensitive(win->menu->add, FALSE);
5353 gtk_action_set_sensitive(win->menu->remove, FALSE);
5354 gtk_action_set_sensitive(win->menu->insert_link, TRUE);
5355 gtk_action_set_sensitive(win->menu->insert_image, FALSE);
5359 * Update the window's icon
5361 if (pidgin_conv_window_is_active_conversation(conv))
5363 GList *l = NULL;
5364 if (PURPLE_IS_IM_CONVERSATION(conv) &&
5365 (gtkconv->u.im->anim))
5367 PurpleBuddy *buddy = purple_blist_find_buddy(purple_conversation_get_account(conv), purple_conversation_get_name(conv));
5368 window_icon =
5369 gdk_pixbuf_animation_get_static_image(gtkconv->u.im->anim);
5371 if (buddy && !PURPLE_BUDDY_IS_ONLINE(buddy))
5372 gdk_pixbuf_saturate_and_pixelate(window_icon, window_icon, 0.0, FALSE);
5374 g_object_ref(window_icon);
5375 l = g_list_append(l, window_icon);
5376 } else {
5377 l = pidgin_conv_get_tab_icons(conv);
5379 gtk_window_set_icon_list(GTK_WINDOW(win->window), l);
5380 if (window_icon != NULL) {
5381 g_object_unref(G_OBJECT(window_icon));
5382 g_list_free(l);
5387 static void
5388 pidgin_conv_update_fields(PurpleConversation *conv, PidginConvFields fields)
5390 PidginConversation *gtkconv;
5391 PidginConvWindow *win;
5393 gtkconv = PIDGIN_CONVERSATION(conv);
5394 if (!gtkconv)
5395 return;
5396 win = pidgin_conv_get_window(gtkconv);
5397 if (!win)
5398 return;
5400 if (fields & PIDGIN_CONV_SET_TITLE)
5402 purple_conversation_autoset_title(conv);
5405 if (fields & PIDGIN_CONV_BUDDY_ICON)
5407 if (PURPLE_IS_IM_CONVERSATION(conv))
5408 pidgin_conv_update_buddy_icon(PURPLE_IM_CONVERSATION(conv));
5411 if (fields & PIDGIN_CONV_MENU)
5413 gray_stuff_out(PIDGIN_CONVERSATION(conv));
5414 generate_send_to_items(win);
5415 regenerate_plugins_items(win);
5418 if (fields & PIDGIN_CONV_E2EE)
5419 generate_e2ee_controls(win);
5421 if (fields & PIDGIN_CONV_TAB_ICON)
5423 update_tab_icon(conv);
5424 generate_send_to_items(win); /* To update the icons in SendTo menu */
5427 if ((fields & PIDGIN_CONV_TOPIC) &&
5428 PURPLE_IS_CHAT_CONVERSATION(conv))
5430 const char *topic;
5431 PidginChatPane *gtkchat = gtkconv->u.chat;
5433 if (gtkchat->topic_text != NULL)
5435 topic = purple_chat_conversation_get_topic(PURPLE_CHAT_CONVERSATION(conv));
5437 gtk_entry_set_text(GTK_ENTRY(gtkchat->topic_text), topic ? topic : "");
5438 gtk_widget_set_tooltip_text(gtkchat->topic_text,
5439 topic ? topic : "");
5443 if ((fields & PIDGIN_CONV_COLORIZE_TITLE) ||
5444 (fields & PIDGIN_CONV_SET_TITLE) ||
5445 (fields & PIDGIN_CONV_TOPIC))
5447 char *title;
5448 PurpleIMConversation *im = NULL;
5449 PurpleAccount *account = purple_conversation_get_account(conv);
5450 PurpleBuddy *buddy = NULL;
5451 char *markup = NULL;
5452 AtkObject *accessibility_obj;
5453 /* I think this is a little longer than it needs to be but I'm lazy. */
5454 char *style;
5456 if (PURPLE_IS_IM_CONVERSATION(conv))
5457 im = PURPLE_IM_CONVERSATION(conv);
5459 if ((account == NULL) ||
5460 !purple_account_is_connected(account) ||
5461 (PURPLE_IS_CHAT_CONVERSATION(conv)
5462 && purple_chat_conversation_has_left(PURPLE_CHAT_CONVERSATION(conv))))
5463 title = g_strdup_printf("(%s)", purple_conversation_get_title(conv));
5464 else
5465 title = g_strdup(purple_conversation_get_title(conv));
5467 if (PURPLE_IS_IM_CONVERSATION(conv)) {
5468 buddy = purple_blist_find_buddy(account, purple_conversation_get_name(conv));
5469 if (buddy) {
5470 markup = pidgin_blist_get_name_markup(buddy, FALSE, FALSE);
5471 } else {
5472 markup = title;
5474 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
5475 const char *topic = gtkconv->u.chat->topic_text
5476 ? gtk_entry_get_text(GTK_ENTRY(gtkconv->u.chat->topic_text))
5477 : NULL;
5478 const char *title = purple_conversation_get_title(conv);
5479 const char *name = purple_conversation_get_name(conv);
5481 char *topic_esc, *unaliased, *unaliased_esc, *title_esc;
5483 topic_esc = topic ? g_markup_escape_text(topic, -1) : NULL;
5484 unaliased = g_utf8_collate(title, name) ? g_strdup_printf("(%s)", name) : NULL;
5485 unaliased_esc = unaliased ? g_markup_escape_text(unaliased, -1) : NULL;
5486 title_esc = g_markup_escape_text(title, -1);
5488 markup = g_strdup_printf("%s%s<span size='smaller'>%s</span>%s<span color='%s' size='smaller'>%s</span>",
5489 title_esc,
5490 unaliased_esc ? " " : "",
5491 unaliased_esc ? unaliased_esc : "",
5492 topic_esc && *topic_esc ? "\n" : "",
5493 pidgin_get_dim_grey_string(gtkconv->infopane),
5494 topic_esc ? topic_esc : "");
5496 g_free(title_esc);
5497 g_free(topic_esc);
5498 g_free(unaliased);
5499 g_free(unaliased_esc);
5501 gtk_list_store_set(gtkconv->infopane_model, &(gtkconv->infopane_iter),
5502 CONV_TEXT_COLUMN, markup, -1);
5503 /* XXX seanegan Why do I have to do this? */
5504 gtk_widget_queue_draw(gtkconv->infopane);
5506 if (title != markup)
5507 g_free(markup);
5509 if (!gtk_widget_get_realized(gtkconv->tab_label))
5510 gtk_widget_realize(gtkconv->tab_label);
5512 accessibility_obj = gtk_widget_get_accessible(gtkconv->tab_cont);
5513 if (im != NULL &&
5514 purple_im_conversation_get_typing_state(im) == PURPLE_IM_TYPING) {
5515 atk_object_set_description(accessibility_obj, _("Typing"));
5516 style = "tab-label-typing";
5517 } else if (im != NULL &&
5518 purple_im_conversation_get_typing_state(im) == PURPLE_IM_TYPED) {
5519 atk_object_set_description(accessibility_obj, _("Stopped Typing"));
5520 style = "tab-label-typed";
5521 } else if (gtkconv->unseen_state == PIDGIN_UNSEEN_NICK) {
5522 atk_object_set_description(accessibility_obj, _("Nick Said"));
5523 style = "tab-label-attention";
5524 } else if (gtkconv->unseen_state == PIDGIN_UNSEEN_TEXT) {
5525 atk_object_set_description(accessibility_obj, _("Unread Messages"));
5526 if (PURPLE_IS_CHAT_CONVERSATION(gtkconv->active_conv))
5527 style = "tab-label-unreadchat";
5528 else
5529 style = "tab-label-attention";
5530 } else if (gtkconv->unseen_state == PIDGIN_UNSEEN_EVENT) {
5531 atk_object_set_description(accessibility_obj, _("New Event"));
5532 style = "tab-label-event";
5533 } else {
5534 style = "tab-label";
5537 gtk_widget_set_name(gtkconv->tab_label, style);
5538 gtk_label_set_text(GTK_LABEL(gtkconv->tab_label), title);
5539 gtk_widget_set_state_flags(gtkconv->tab_label, GTK_STATE_FLAG_ACTIVE, TRUE);
5541 if (gtkconv->unseen_state == PIDGIN_UNSEEN_TEXT ||
5542 gtkconv->unseen_state == PIDGIN_UNSEEN_NICK ||
5543 gtkconv->unseen_state == PIDGIN_UNSEEN_EVENT) {
5544 PangoAttrList *list = pango_attr_list_new();
5545 PangoAttribute *attr = pango_attr_weight_new(PANGO_WEIGHT_BOLD);
5546 attr->start_index = 0;
5547 attr->end_index = -1;
5548 pango_attr_list_insert(list, attr);
5549 gtk_label_set_attributes(GTK_LABEL(gtkconv->tab_label), list);
5550 pango_attr_list_unref(list);
5551 } else
5552 gtk_label_set_attributes(GTK_LABEL(gtkconv->tab_label), NULL);
5554 if (pidgin_conv_window_is_active_conversation(conv))
5555 update_typing_icon(gtkconv);
5557 gtk_label_set_text(GTK_LABEL(gtkconv->menu_label), title);
5558 if (pidgin_conv_window_is_active_conversation(conv)) {
5559 const char* current_title = gtk_window_get_title(GTK_WINDOW(win->window));
5560 if (current_title == NULL || !purple_strequal(current_title, title))
5561 gtk_window_set_title(GTK_WINDOW(win->window), title);
5564 g_free(title);
5568 static void
5569 pidgin_conv_updated(PurpleConversation *conv, PurpleConversationUpdateType type)
5571 PidginConvFields flags = 0;
5573 g_return_if_fail(conv != NULL);
5575 if (type == PURPLE_CONVERSATION_UPDATE_ACCOUNT)
5577 flags = PIDGIN_CONV_ALL;
5579 else if (type == PURPLE_CONVERSATION_UPDATE_TYPING ||
5580 type == PURPLE_CONVERSATION_UPDATE_UNSEEN ||
5581 type == PURPLE_CONVERSATION_UPDATE_TITLE)
5583 flags = PIDGIN_CONV_COLORIZE_TITLE;
5585 else if (type == PURPLE_CONVERSATION_UPDATE_TOPIC)
5587 flags = PIDGIN_CONV_TOPIC;
5589 else if (type == PURPLE_CONVERSATION_ACCOUNT_ONLINE ||
5590 type == PURPLE_CONVERSATION_ACCOUNT_OFFLINE)
5592 flags = PIDGIN_CONV_MENU | PIDGIN_CONV_TAB_ICON | PIDGIN_CONV_SET_TITLE;
5594 else if (type == PURPLE_CONVERSATION_UPDATE_AWAY)
5596 flags = PIDGIN_CONV_TAB_ICON;
5598 else if (type == PURPLE_CONVERSATION_UPDATE_ADD ||
5599 type == PURPLE_CONVERSATION_UPDATE_REMOVE ||
5600 type == PURPLE_CONVERSATION_UPDATE_CHATLEFT)
5602 flags = PIDGIN_CONV_SET_TITLE | PIDGIN_CONV_MENU;
5604 else if (type == PURPLE_CONVERSATION_UPDATE_ICON)
5606 flags = PIDGIN_CONV_BUDDY_ICON;
5608 else if (type == PURPLE_CONVERSATION_UPDATE_FEATURES)
5610 flags = PIDGIN_CONV_MENU;
5612 else if (type == PURPLE_CONVERSATION_UPDATE_E2EE)
5614 flags = PIDGIN_CONV_E2EE | PIDGIN_CONV_MENU;
5617 pidgin_conv_update_fields(conv, flags);
5620 static void
5621 wrote_msg_update_unseen_cb(PurpleConversation *conv, PurpleMessage *msg,
5622 gpointer _unused)
5624 PidginConversation *gtkconv = conv ? PIDGIN_CONVERSATION(conv) : NULL;
5625 PurpleMessageFlags flags;
5626 if (conv == NULL || (gtkconv && gtkconv->win != hidden_convwin))
5627 return;
5628 flags = purple_message_get_flags(msg);
5629 if (flags & (PURPLE_MESSAGE_SEND | PURPLE_MESSAGE_RECV)) {
5630 PidginUnseenState unseen = PIDGIN_UNSEEN_NONE;
5632 if ((flags & PURPLE_MESSAGE_NICK) == PURPLE_MESSAGE_NICK)
5633 unseen = PIDGIN_UNSEEN_NICK;
5634 else if (((flags & PURPLE_MESSAGE_SYSTEM) == PURPLE_MESSAGE_SYSTEM) ||
5635 ((flags & PURPLE_MESSAGE_ERROR) == PURPLE_MESSAGE_ERROR))
5636 unseen = PIDGIN_UNSEEN_EVENT;
5637 else if ((flags & PURPLE_MESSAGE_NO_LOG) == PURPLE_MESSAGE_NO_LOG)
5638 unseen = PIDGIN_UNSEEN_NO_LOG;
5639 else
5640 unseen = PIDGIN_UNSEEN_TEXT;
5642 conv_set_unseen(conv, unseen);
5646 static PurpleConversationUiOps conversation_ui_ops =
5648 pidgin_conv_new,
5649 pidgin_conv_destroy, /* destroy_conversation */
5650 NULL, /* write_chat */
5651 NULL, /* write_im */
5652 pidgin_conv_write_conv, /* write_conv */
5653 pidgin_conv_chat_add_users, /* chat_add_users */
5654 pidgin_conv_chat_rename_user, /* chat_rename_user */
5655 pidgin_conv_chat_remove_users, /* chat_remove_users */
5656 pidgin_conv_chat_update_user, /* chat_update_user */
5657 pidgin_conv_present_conversation, /* present */
5658 pidgin_conv_has_focus, /* has_focus */
5659 NULL, /* send_confirm */
5660 NULL,
5661 NULL,
5662 NULL,
5663 NULL
5666 PurpleConversationUiOps *
5667 pidgin_conversations_get_conv_ui_ops(void)
5669 return &conversation_ui_ops;
5672 /**************************************************************************
5673 * Public conversation utility functions
5674 **************************************************************************/
5675 void
5676 pidgin_conv_update_buddy_icon(PurpleIMConversation *im)
5678 PidginConversation *gtkconv;
5679 PurpleConversation *conv;
5680 PidginConvWindow *win;
5682 PurpleBuddy *buddy;
5684 PurpleImage *custom_img = NULL;
5685 gconstpointer data = NULL;
5686 size_t len;
5688 GdkPixbuf *buf;
5690 GList *children;
5691 GtkWidget *event;
5692 GdkPixbuf *scale;
5693 int scale_width, scale_height;
5694 int size = 0;
5696 PurpleAccount *account;
5698 PurpleBuddyIcon *icon;
5700 conv = PURPLE_CONVERSATION(im);
5702 g_return_if_fail(conv != NULL);
5703 g_return_if_fail(PIDGIN_IS_PIDGIN_CONVERSATION(conv));
5705 gtkconv = PIDGIN_CONVERSATION(conv);
5706 win = gtkconv->win;
5707 if (conv != gtkconv->active_conv)
5708 return;
5710 if (!gtkconv->u.im->show_icon)
5711 return;
5713 account = purple_conversation_get_account(conv);
5715 /* Remove the current icon stuff */
5716 children = gtk_container_get_children(GTK_CONTAINER(gtkconv->u.im->icon_container));
5717 if (children) {
5718 /* We know there's only one child here. It'd be nice to shortcut to the
5719 event box, but we can't change the PidginConversation until 3.0 */
5720 event = (GtkWidget *)children->data;
5721 gtk_container_remove(GTK_CONTAINER(gtkconv->u.im->icon_container), event);
5722 g_list_free(children);
5725 if (gtkconv->u.im->anim != NULL)
5726 g_object_unref(G_OBJECT(gtkconv->u.im->anim));
5728 gtkconv->u.im->anim = NULL;
5730 if (gtkconv->u.im->icon_timer != 0)
5731 g_source_remove(gtkconv->u.im->icon_timer);
5733 gtkconv->u.im->icon_timer = 0;
5735 if (gtkconv->u.im->iter != NULL)
5736 g_object_unref(G_OBJECT(gtkconv->u.im->iter));
5738 gtkconv->u.im->iter = NULL;
5740 if (!purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/im/show_buddy_icons"))
5741 return;
5743 if (purple_conversation_get_connection(conv) == NULL)
5744 return;
5746 buddy = purple_blist_find_buddy(account, purple_conversation_get_name(conv));
5747 if (buddy)
5749 PurpleContact *contact = purple_buddy_get_contact(buddy);
5750 if (contact) {
5751 custom_img = purple_buddy_icons_node_find_custom_icon((PurpleBlistNode*)contact);
5752 if (custom_img) {
5753 /* There is a custom icon for this user */
5754 data = purple_image_get_data(custom_img);
5755 len = purple_image_get_data_size(custom_img);
5760 if (data == NULL) {
5761 icon = purple_im_conversation_get_icon(im);
5762 if (icon == NULL)
5764 gtk_widget_set_size_request(gtkconv->u.im->icon_container,
5765 -1, BUDDYICON_SIZE_MIN);
5766 return;
5769 data = purple_buddy_icon_get_data(icon, &len);
5770 if (data == NULL)
5772 gtk_widget_set_size_request(gtkconv->u.im->icon_container,
5773 -1, BUDDYICON_SIZE_MIN);
5774 return;
5778 gtkconv->u.im->anim = pidgin_pixbuf_anim_from_data(data, len);
5779 if (custom_img)
5780 g_object_unref(custom_img);
5782 if (!gtkconv->u.im->anim) {
5783 purple_debug_error("gtkconv", "Couldn't load icon for conv %s\n",
5784 purple_conversation_get_name(conv));
5785 return;
5788 if (gdk_pixbuf_animation_is_static_image(gtkconv->u.im->anim)) {
5789 GdkPixbuf *stat;
5790 gtkconv->u.im->iter = NULL;
5791 stat = gdk_pixbuf_animation_get_static_image(gtkconv->u.im->anim);
5792 buf = gdk_pixbuf_add_alpha(stat, FALSE, 0, 0, 0);
5793 } else {
5794 GdkPixbuf *stat;
5795 gtkconv->u.im->iter =
5796 gdk_pixbuf_animation_get_iter(gtkconv->u.im->anim, NULL); /* LEAK */
5797 stat = gdk_pixbuf_animation_iter_get_pixbuf(gtkconv->u.im->iter);
5798 buf = gdk_pixbuf_add_alpha(stat, FALSE, 0, 0, 0);
5799 if (gtkconv->u.im->animate)
5800 start_anim(NULL, gtkconv);
5803 scale_width = gdk_pixbuf_get_width(buf);
5804 scale_height = gdk_pixbuf_get_height(buf);
5806 gtk_widget_get_size_request(gtkconv->u.im->icon_container, NULL, &size);
5807 size = MIN(size, MIN(scale_width, scale_height));
5809 /* Some sanity checks */
5810 size = CLAMP(size, BUDDYICON_SIZE_MIN, BUDDYICON_SIZE_MAX);
5811 if (scale_width == scale_height) {
5812 scale_width = scale_height = size;
5813 } else if (scale_height > scale_width) {
5814 scale_width = size * scale_width / scale_height;
5815 scale_height = size;
5816 } else {
5817 scale_height = size * scale_height / scale_width;
5818 scale_width = size;
5820 scale = gdk_pixbuf_scale_simple(buf, scale_width, scale_height,
5821 GDK_INTERP_BILINEAR);
5822 g_object_unref(buf);
5823 if (pidgin_gdk_pixbuf_is_opaque(scale))
5824 pidgin_gdk_pixbuf_make_round(scale);
5826 event = gtk_event_box_new();
5827 gtk_container_add(GTK_CONTAINER(gtkconv->u.im->icon_container), event);
5828 gtk_event_box_set_visible_window(GTK_EVENT_BOX(event), FALSE);
5829 gtk_widget_add_events(event,
5830 GDK_POINTER_MOTION_MASK | GDK_LEAVE_NOTIFY_MASK);
5831 g_signal_connect(G_OBJECT(event), "button-press-event",
5832 G_CALLBACK(icon_menu), gtkconv);
5834 pidgin_tooltip_setup_for_widget(event, gtkconv, pidgin_conv_create_tooltip, NULL);
5835 gtk_widget_show(event);
5837 gtkconv->u.im->icon = gtk_image_new_from_pixbuf(scale);
5838 gtk_container_add(GTK_CONTAINER(event), gtkconv->u.im->icon);
5839 gtk_widget_show(gtkconv->u.im->icon);
5841 g_object_unref(G_OBJECT(scale));
5843 /* The buddy icon code needs badly to be fixed. */
5844 if(pidgin_conv_window_is_active_conversation(conv))
5846 buf = gdk_pixbuf_animation_get_static_image(gtkconv->u.im->anim);
5847 if (buddy && !PURPLE_BUDDY_IS_ONLINE(buddy))
5848 gdk_pixbuf_saturate_and_pixelate(buf, buf, 0.0, FALSE);
5849 gtk_window_set_icon(GTK_WINDOW(win->window), buf);
5853 void
5854 pidgin_conv_update_buttons_by_protocol(PurpleConversation *conv)
5856 PidginConvWindow *win;
5858 if (!PIDGIN_IS_PIDGIN_CONVERSATION(conv))
5859 return;
5861 win = PIDGIN_CONVERSATION(conv)->win;
5863 if (win != NULL && pidgin_conv_window_is_active_conversation(conv))
5864 gray_stuff_out(PIDGIN_CONVERSATION(conv));
5867 static gboolean
5868 pidgin_conv_xy_to_right_infopane(PidginConvWindow *win, int x, int y)
5870 gint pane_x, pane_y, x_rel;
5871 PidginConversation *gtkconv;
5872 GtkAllocation allocation;
5874 gdk_window_get_origin(gtk_widget_get_window(win->notebook),
5875 &pane_x, &pane_y);
5876 x_rel = x - pane_x;
5877 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
5878 gtk_widget_get_allocation(gtkconv->infopane, &allocation);
5879 return (x_rel > allocation.x + allocation.width / 2);
5883 pidgin_conv_get_tab_at_xy(PidginConvWindow *win, int x, int y, gboolean *to_right)
5885 gint nb_x, nb_y, x_rel, y_rel;
5886 GtkNotebook *notebook;
5887 GtkWidget *page, *tab;
5888 gint i, page_num = -1;
5889 gint count;
5890 gboolean horiz;
5892 if (to_right)
5893 *to_right = FALSE;
5895 notebook = GTK_NOTEBOOK(win->notebook);
5897 gdk_window_get_origin(gtk_widget_get_window(win->notebook), &nb_x, &nb_y);
5898 x_rel = x - nb_x;
5899 y_rel = y - nb_y;
5901 horiz = (gtk_notebook_get_tab_pos(notebook) == GTK_POS_TOP ||
5902 gtk_notebook_get_tab_pos(notebook) == GTK_POS_BOTTOM);
5904 count = gtk_notebook_get_n_pages(GTK_NOTEBOOK(notebook));
5906 for (i = 0; i < count; i++) {
5907 GtkAllocation allocation;
5909 page = gtk_notebook_get_nth_page(GTK_NOTEBOOK(notebook), i);
5910 tab = gtk_notebook_get_tab_label(GTK_NOTEBOOK(notebook), page);
5911 gtk_widget_get_allocation(tab, &allocation);
5913 /* Make sure the tab is not hidden beyond an arrow */
5914 if (!gtk_widget_is_drawable(tab) && gtk_notebook_get_show_tabs(notebook))
5915 continue;
5917 if (horiz) {
5918 if (x_rel >= allocation.x - PIDGIN_HIG_BOX_SPACE &&
5919 x_rel <= allocation.x + allocation.width + PIDGIN_HIG_BOX_SPACE) {
5920 page_num = i;
5922 if (to_right && x_rel >= allocation.x + allocation.width/2)
5923 *to_right = TRUE;
5925 break;
5927 } else {
5928 if (y_rel >= allocation.y - PIDGIN_HIG_BOX_SPACE &&
5929 y_rel <= allocation.y + allocation.height + PIDGIN_HIG_BOX_SPACE) {
5930 page_num = i;
5932 if (to_right && y_rel >= allocation.y + allocation.height/2)
5933 *to_right = TRUE;
5935 break;
5940 if (page_num == -1) {
5941 /* Add after the last tab */
5942 page_num = count - 1;
5945 return page_num;
5948 static void
5949 close_on_tabs_pref_cb(const char *name, PurplePrefType type,
5950 gconstpointer value, gpointer data)
5952 GList *l;
5953 PurpleConversation *conv;
5954 PidginConversation *gtkconv;
5956 for (l = purple_conversations_get_all(); l != NULL; l = l->next) {
5957 conv = (PurpleConversation *)l->data;
5959 if (!PIDGIN_IS_PIDGIN_CONVERSATION(conv))
5960 continue;
5962 gtkconv = PIDGIN_CONVERSATION(conv);
5964 if (value)
5965 gtk_widget_show(gtkconv->close);
5966 else
5967 gtk_widget_hide(gtkconv->close);
5971 static void
5972 spellcheck_pref_cb(const char *name, PurplePrefType type,
5973 gconstpointer value, gpointer data)
5975 GList *cl;
5976 PurpleConversation *conv;
5977 PidginConversation *gtkconv;
5979 for (cl = purple_conversations_get_all(); cl != NULL; cl = cl->next) {
5981 conv = (PurpleConversation *)cl->data;
5983 if (!PIDGIN_IS_PIDGIN_CONVERSATION(conv))
5984 continue;
5986 gtkconv = PIDGIN_CONVERSATION(conv);
5988 # warning toggle spell checking when talkatu #60 is done.
5992 static void
5993 tab_side_pref_cb(const char *name, PurplePrefType type,
5994 gconstpointer value, gpointer data)
5996 GList *gtkwins, *gtkconvs;
5997 GtkPositionType pos;
5998 PidginConvWindow *gtkwin;
6000 pos = GPOINTER_TO_INT(value);
6002 for (gtkwins = pidgin_conv_windows_get_list(); gtkwins != NULL; gtkwins = gtkwins->next) {
6003 gtkwin = gtkwins->data;
6004 gtk_notebook_set_tab_pos(GTK_NOTEBOOK(gtkwin->notebook), pos&~8);
6005 for (gtkconvs = gtkwin->gtkconvs; gtkconvs != NULL; gtkconvs = gtkconvs->next) {
6006 pidgin_conv_tab_pack(gtkwin, gtkconvs->data);
6011 static void
6012 show_formatting_toolbar_pref_cb(const char *name, PurplePrefType type,
6013 gconstpointer value, gpointer data)
6015 GList *l;
6016 PurpleConversation *conv;
6017 PidginConversation *gtkconv;
6018 PidginConvWindow *win;
6019 gboolean visible = (gboolean)GPOINTER_TO_INT(value);
6021 for (l = purple_conversations_get_all(); l != NULL; l = l->next)
6023 conv = (PurpleConversation *)l->data;
6025 if (!PIDGIN_IS_PIDGIN_CONVERSATION(conv))
6026 continue;
6028 gtkconv = PIDGIN_CONVERSATION(conv);
6029 win = gtkconv->win;
6031 gtk_toggle_action_set_active(
6032 GTK_TOGGLE_ACTION(win->menu->show_formatting_toolbar),
6033 visible
6036 talkatu_editor_set_toolbar_visible(TALKATU_EDITOR(gtkconv->editor), visible);
6040 static void
6041 animate_buddy_icons_pref_cb(const char *name, PurplePrefType type,
6042 gconstpointer value, gpointer data)
6044 GList *l;
6045 PurpleConversation *conv;
6046 PidginConversation *gtkconv;
6047 PidginConvWindow *win;
6049 if (!purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/im/show_buddy_icons"))
6050 return;
6052 /* Set the "animate" flag for each icon based on the new preference */
6053 for (l = purple_conversations_get_ims(); l != NULL; l = l->next) {
6054 conv = (PurpleConversation *)l->data;
6055 gtkconv = PIDGIN_CONVERSATION(conv);
6056 if (gtkconv)
6057 gtkconv->u.im->animate = GPOINTER_TO_INT(value);
6060 /* Now either stop or start animation for the active conversation in each window */
6061 for (l = pidgin_conv_windows_get_list(); l != NULL; l = l->next) {
6062 win = l->data;
6063 conv = pidgin_conv_window_get_active_conversation(win);
6064 pidgin_conv_update_buddy_icon(PURPLE_IM_CONVERSATION(conv));
6068 static void
6069 show_buddy_icons_pref_cb(const char *name, PurplePrefType type,
6070 gconstpointer value, gpointer data)
6072 GList *l;
6074 for (l = purple_conversations_get_all(); l != NULL; l = l->next) {
6075 PurpleConversation *conv = l->data;
6076 if (!PIDGIN_CONVERSATION(conv))
6077 continue;
6078 if (GPOINTER_TO_INT(value))
6079 gtk_widget_show(PIDGIN_CONVERSATION(conv)->infopane_hbox);
6080 else
6081 gtk_widget_hide(PIDGIN_CONVERSATION(conv)->infopane_hbox);
6083 if (PURPLE_IS_IM_CONVERSATION(conv)) {
6084 pidgin_conv_update_buddy_icon(PURPLE_IM_CONVERSATION(conv));
6088 /* Make the tabs show/hide correctly */
6089 for (l = pidgin_conv_windows_get_list(); l != NULL; l = l->next) {
6090 PidginConvWindow *win = l->data;
6091 if (pidgin_conv_window_get_gtkconv_count(win) == 1)
6092 gtk_notebook_set_show_tabs(GTK_NOTEBOOK(win->notebook),
6093 GPOINTER_TO_INT(value) == 0);
6097 static void
6098 show_protocol_icons_pref_cb(const char *name, PurplePrefType type,
6099 gconstpointer value, gpointer data)
6101 GList *l;
6102 for (l = purple_conversations_get_all(); l != NULL; l = l->next) {
6103 PurpleConversation *conv = l->data;
6104 if (PIDGIN_CONVERSATION(conv))
6105 update_tab_icon(conv);
6109 static void
6110 conv_placement_usetabs_cb(const char *name, PurplePrefType type,
6111 gconstpointer value, gpointer data)
6113 purple_prefs_trigger_callback(PIDGIN_PREFS_ROOT "/conversations/placement");
6116 static void
6117 account_status_changed_cb(PurpleAccount *account, PurpleStatus *oldstatus,
6118 PurpleStatus *newstatus)
6120 GList *l;
6121 PurpleConversation *conv = NULL;
6122 PidginConversation *gtkconv;
6124 if(!purple_strequal(purple_prefs_get_string(PIDGIN_PREFS_ROOT "/conversations/im/hide_new"), "away"))
6125 return;
6127 if(purple_status_is_available(oldstatus) || !purple_status_is_available(newstatus))
6128 return;
6130 for (l = hidden_convwin->gtkconvs; l; ) {
6131 gtkconv = l->data;
6132 l = l->next;
6134 conv = gtkconv->active_conv;
6135 if (PURPLE_IS_CHAT_CONVERSATION(conv) ||
6136 account != purple_conversation_get_account(conv))
6137 continue;
6139 pidgin_conv_attach_to_conversation(conv);
6141 /* TODO: do we need to do anything for any other conversations that are in the same gtkconv here?
6142 * I'm a little concerned that not doing so will cause the "pending" indicator in the gtkblist not to be cleared. -DAA*/
6143 purple_conversation_update(conv, PURPLE_CONVERSATION_UPDATE_UNSEEN);
6147 static void
6148 hide_new_pref_cb(const char *name, PurplePrefType type,
6149 gconstpointer value, gpointer data)
6151 GList *l;
6152 PurpleConversation *conv = NULL;
6153 PidginConversation *gtkconv;
6154 gboolean when_away = FALSE;
6156 if(!hidden_convwin)
6157 return;
6159 if(purple_strequal(purple_prefs_get_string(PIDGIN_PREFS_ROOT "/conversations/im/hide_new"), "always"))
6160 return;
6162 if(purple_strequal(purple_prefs_get_string(PIDGIN_PREFS_ROOT "/conversations/im/hide_new"), "away"))
6163 when_away = TRUE;
6165 for (l = hidden_convwin->gtkconvs; l; )
6167 gtkconv = l->data;
6168 l = l->next;
6170 conv = gtkconv->active_conv;
6172 if (PURPLE_IS_CHAT_CONVERSATION(conv) ||
6173 gtkconv->unseen_count == 0 ||
6174 (when_away && !purple_status_is_available(
6175 purple_account_get_active_status(
6176 purple_conversation_get_account(conv)))))
6177 continue;
6179 pidgin_conv_attach_to_conversation(conv);
6184 static void
6185 conv_placement_pref_cb(const char *name, PurplePrefType type,
6186 gconstpointer value, gpointer data)
6188 PidginConvPlacementFunc func;
6190 if (!purple_strequal(name, PIDGIN_PREFS_ROOT "/conversations/placement"))
6191 return;
6193 func = pidgin_conv_placement_get_fnc(value);
6195 if (func == NULL)
6196 return;
6198 pidgin_conv_placement_set_current_func(func);
6201 static PidginConversation *
6202 get_gtkconv_with_contact(PurpleContact *contact)
6204 PurpleBlistNode *node;
6206 node = ((PurpleBlistNode*)contact)->child;
6208 for (; node; node = node->next)
6210 PurpleBuddy *buddy = (PurpleBuddy*)node;
6211 PurpleIMConversation *im;
6212 im = purple_conversations_find_im_with_account(purple_buddy_get_name(buddy), purple_buddy_get_account(buddy));
6213 if (im)
6214 return PIDGIN_CONVERSATION(PURPLE_CONVERSATION(im));
6216 return NULL;
6219 static void
6220 account_signed_off_cb(PurpleConnection *gc, gpointer event)
6222 GList *iter;
6224 for (iter = purple_conversations_get_all(); iter; iter = iter->next)
6226 PurpleConversation *conv = iter->data;
6228 /* This seems fine in theory, but we also need to cover the
6229 * case of this account matching one of the other buddies in
6230 * one of the contacts containing the buddy corresponding to
6231 * a conversation. It's easier to just update them all. */
6232 /* if (purple_conversation_get_account(conv) == account) */
6233 pidgin_conv_update_fields(conv, PIDGIN_CONV_TAB_ICON |
6234 PIDGIN_CONV_MENU | PIDGIN_CONV_COLORIZE_TITLE);
6236 if (PURPLE_CONNECTION_IS_CONNECTED(gc) &&
6237 PURPLE_IS_CHAT_CONVERSATION(conv) &&
6238 purple_conversation_get_account(conv) == purple_connection_get_account(gc) &&
6239 g_object_get_data(G_OBJECT(conv), "want-to-rejoin")) {
6240 GHashTable *comps = NULL;
6241 PurpleChat *chat = purple_blist_find_chat(purple_conversation_get_account(conv), purple_conversation_get_name(conv));
6242 if (chat == NULL) {
6243 PurpleProtocol *protocol = purple_connection_get_protocol(gc);
6244 comps = purple_protocol_chat_iface_info_defaults(protocol, gc, purple_conversation_get_name(conv));
6245 } else {
6246 comps = purple_chat_get_components(chat);
6248 purple_serv_join_chat(gc, comps);
6249 if (chat == NULL && comps != NULL)
6250 g_hash_table_destroy(comps);
6255 static void
6256 account_signing_off(PurpleConnection *gc)
6258 GList *list = purple_conversations_get_chats();
6259 PurpleAccount *account = purple_connection_get_account(gc);
6261 /* We are about to sign off. See which chats we are currently in, and mark
6262 * them for rejoin on reconnect. */
6263 while (list) {
6264 PurpleConversation *conv = list->data;
6265 if (!purple_chat_conversation_has_left(PURPLE_CHAT_CONVERSATION(conv)) &&
6266 purple_conversation_get_account(conv) == account) {
6267 g_object_set_data(G_OBJECT(conv), "want-to-rejoin", GINT_TO_POINTER(TRUE));
6268 purple_conversation_write_system_message(conv,
6269 _("The account has disconnected and you are no "
6270 "longer in this chat. You will automatically "
6271 "rejoin the chat when the account reconnects."),
6272 PURPLE_MESSAGE_NO_LOG);
6274 list = list->next;
6278 static void
6279 update_buddy_status_changed(PurpleBuddy *buddy, PurpleStatus *old, PurpleStatus *newstatus)
6281 PidginConversation *gtkconv;
6282 PurpleConversation *conv;
6284 gtkconv = get_gtkconv_with_contact(purple_buddy_get_contact(buddy));
6285 if (gtkconv)
6287 conv = gtkconv->active_conv;
6288 pidgin_conv_update_fields(conv, PIDGIN_CONV_TAB_ICON
6289 | PIDGIN_CONV_COLORIZE_TITLE
6290 | PIDGIN_CONV_BUDDY_ICON);
6291 if ((purple_status_is_online(old) ^ purple_status_is_online(newstatus)) != 0)
6292 pidgin_conv_update_fields(conv, PIDGIN_CONV_MENU);
6296 static void
6297 update_buddy_privacy_changed(PurpleBuddy *buddy)
6299 PidginConversation *gtkconv;
6300 PurpleConversation *conv;
6302 gtkconv = get_gtkconv_with_contact(purple_buddy_get_contact(buddy));
6303 if (gtkconv) {
6304 conv = gtkconv->active_conv;
6305 pidgin_conv_update_fields(conv, PIDGIN_CONV_TAB_ICON | PIDGIN_CONV_MENU);
6309 static void
6310 update_buddy_idle_changed(PurpleBuddy *buddy, gboolean old, gboolean newidle)
6312 PurpleIMConversation *im;
6314 im = purple_conversations_find_im_with_account(purple_buddy_get_name(buddy), purple_buddy_get_account(buddy));
6315 if (im)
6316 pidgin_conv_update_fields(PURPLE_CONVERSATION(im), PIDGIN_CONV_TAB_ICON);
6319 static void
6320 update_buddy_icon(PurpleBuddy *buddy)
6322 PurpleIMConversation *im;
6324 im = purple_conversations_find_im_with_account(purple_buddy_get_name(buddy), purple_buddy_get_account(buddy));
6325 if (im)
6326 pidgin_conv_update_fields(PURPLE_CONVERSATION(im), PIDGIN_CONV_BUDDY_ICON);
6329 static void
6330 update_buddy_sign(PurpleBuddy *buddy, const char *which)
6332 PurplePresence *presence;
6333 PurpleStatus *on, *off;
6335 presence = purple_buddy_get_presence(buddy);
6336 if (!presence)
6337 return;
6338 off = purple_presence_get_status(presence, "offline");
6339 on = purple_presence_get_status(presence, "available");
6341 if (*(which+1) == 'f')
6342 update_buddy_status_changed(buddy, on, off);
6343 else
6344 update_buddy_status_changed(buddy, off, on);
6347 static void
6348 update_conversation_switched(PurpleConversation *conv)
6350 pidgin_conv_update_fields(conv, PIDGIN_CONV_TAB_ICON |
6351 PIDGIN_CONV_SET_TITLE | PIDGIN_CONV_MENU |
6352 PIDGIN_CONV_BUDDY_ICON | PIDGIN_CONV_E2EE );
6355 static void
6356 update_buddy_typing(PurpleAccount *account, const char *who)
6358 PurpleConversation *conv;
6359 PidginConversation *gtkconv;
6361 conv = PURPLE_CONVERSATION(purple_conversations_find_im_with_account(who, account));
6362 if (!conv)
6363 return;
6365 gtkconv = PIDGIN_CONVERSATION(conv);
6366 if (gtkconv && gtkconv->active_conv == conv)
6367 pidgin_conv_update_fields(conv, PIDGIN_CONV_COLORIZE_TITLE);
6370 static void
6371 update_chat(PurpleChatConversation *chat)
6373 pidgin_conv_update_fields(PURPLE_CONVERSATION(chat), PIDGIN_CONV_TOPIC |
6374 PIDGIN_CONV_MENU | PIDGIN_CONV_SET_TITLE);
6377 static void
6378 update_chat_topic(PurpleChatConversation *chat, const char *old, const char *new)
6380 pidgin_conv_update_fields(PURPLE_CONVERSATION(chat), PIDGIN_CONV_TOPIC);
6383 /* Message history stuff */
6385 /* Compare two PurpleMessages, according to time in ascending order. */
6386 static int
6387 message_compare(PurpleMessage *m1, PurpleMessage *m2)
6389 guint64 t1 = purple_message_get_time(m1), t2 = purple_message_get_time(m2);
6390 return (t1 > t2) - (t1 < t2);
6393 /* Adds some message history to the gtkconv. This happens in a idle-callback. */
6394 static gboolean
6395 add_message_history_to_gtkconv(gpointer data)
6397 PidginConversation *gtkconv = data;
6398 int count = 0;
6399 int timer = gtkconv->attach_timer;
6400 time_t when = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(gtkconv->editor), "attach-start-time"));
6401 gboolean im = (PURPLE_IS_IM_CONVERSATION(gtkconv->active_conv));
6403 gtkconv->attach_timer = 0;
6404 while (gtkconv->attach_current && count < ADD_MESSAGE_HISTORY_AT_ONCE) {
6405 PurpleMessage *msg = gtkconv->attach_current->data;
6406 if (!im && when && (guint64)when < purple_message_get_time(msg)) {
6407 g_object_set_data(G_OBJECT(gtkconv->editor), "attach-start-time", NULL);
6409 /* XXX: should it be gtkconv->active_conv? */
6410 pidgin_conv_write_conv(gtkconv->active_conv, msg);
6411 if (im) {
6412 gtkconv->attach_current = g_list_delete_link(gtkconv->attach_current, gtkconv->attach_current);
6413 } else {
6414 gtkconv->attach_current = gtkconv->attach_current->prev;
6416 count++;
6418 gtkconv->attach_timer = timer;
6419 if (gtkconv->attach_current)
6420 return TRUE;
6422 g_source_remove(gtkconv->attach_timer);
6423 gtkconv->attach_timer = 0;
6424 if (im) {
6425 /* Print any message that was sent while the old history was being added back. */
6426 GList *msgs = NULL;
6427 GList *iter = gtkconv->convs;
6428 for (; iter; iter = iter->next) {
6429 PurpleConversation *conv = iter->data;
6430 GList *history = purple_conversation_get_message_history(conv);
6431 for (; history; history = history->next) {
6432 PurpleMessage *msg = history->data;
6433 if (purple_message_get_time(msg) > (guint64)when)
6434 msgs = g_list_prepend(msgs, msg);
6437 msgs = g_list_sort(msgs, (GCompareFunc)message_compare);
6438 for (; msgs; msgs = g_list_delete_link(msgs, msgs)) {
6439 PurpleMessage *msg = msgs->data;
6440 /* XXX: see above - should it be active_conv? */
6441 pidgin_conv_write_conv(gtkconv->active_conv, msg);
6443 g_object_set_data(G_OBJECT(gtkconv->editor), "attach-start-time", NULL);
6446 g_object_set_data(G_OBJECT(gtkconv->editor), "attach-start-time", NULL);
6447 purple_signal_emit(pidgin_conversations_get_handle(),
6448 "conversation-displayed", gtkconv);
6449 return FALSE;
6452 static void
6453 pidgin_conv_attach(PurpleConversation *conv)
6455 int timer;
6456 g_object_set_data(G_OBJECT(conv), "unseen-count", NULL);
6457 g_object_set_data(G_OBJECT(conv), "unseen-state", NULL);
6458 purple_conversation_set_ui_ops(conv, pidgin_conversations_get_conv_ui_ops());
6459 if (!PIDGIN_CONVERSATION(conv))
6460 private_gtkconv_new(conv, FALSE);
6461 timer = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(conv), "close-timer"));
6462 if (timer) {
6463 g_source_remove(timer);
6464 g_object_set_data(G_OBJECT(conv), "close-timer", NULL);
6468 gboolean pidgin_conv_attach_to_conversation(PurpleConversation *conv)
6470 GList *list;
6471 PidginConversation *gtkconv;
6473 if (PIDGIN_IS_PIDGIN_CONVERSATION(conv)) {
6474 /* This is pretty much always the case now. */
6475 gtkconv = PIDGIN_CONVERSATION(conv);
6476 if (gtkconv->win != hidden_convwin)
6477 return FALSE;
6478 pidgin_conv_window_remove_gtkconv(hidden_convwin, gtkconv);
6479 pidgin_conv_placement_place(gtkconv);
6480 purple_signal_emit(pidgin_conversations_get_handle(),
6481 "conversation-displayed", gtkconv);
6482 list = gtkconv->convs;
6483 while (list) {
6484 pidgin_conv_attach(list->data);
6485 list = list->next;
6487 return TRUE;
6490 pidgin_conv_attach(conv);
6491 gtkconv = PIDGIN_CONVERSATION(conv);
6493 list = purple_conversation_get_message_history(conv);
6494 if (list) {
6495 if (PURPLE_IS_IM_CONVERSATION(conv)) {
6496 GList *convs;
6497 list = g_list_copy(list);
6498 for (convs = purple_conversations_get_ims(); convs; convs = convs->next)
6499 if (convs->data != conv &&
6500 pidgin_conv_find_gtkconv(convs->data) == gtkconv) {
6501 pidgin_conv_attach(convs->data);
6502 list = g_list_concat(list, g_list_copy(purple_conversation_get_message_history(convs->data)));
6504 list = g_list_sort(list, (GCompareFunc)message_compare);
6505 gtkconv->attach_current = list;
6506 list = g_list_last(list);
6507 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
6508 gtkconv->attach_current = g_list_last(list);
6511 g_object_set_data(G_OBJECT(gtkconv->editor), "attach-start-time",
6512 GINT_TO_POINTER(purple_message_get_time(list->data)));
6513 gtkconv->attach_timer = g_idle_add(add_message_history_to_gtkconv, gtkconv);
6514 } else {
6515 purple_signal_emit(pidgin_conversations_get_handle(),
6516 "conversation-displayed", gtkconv);
6519 if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
6520 GList *users;
6521 PurpleChatConversation *chat = PURPLE_CHAT_CONVERSATION(conv);
6522 pidgin_conv_update_fields(conv, PIDGIN_CONV_TOPIC);
6523 users = purple_chat_conversation_get_users(chat);
6524 pidgin_conv_chat_add_users(chat, users, TRUE);
6525 g_list_free(users);
6528 return TRUE;
6531 void *
6532 pidgin_conversations_get_handle(void)
6534 static int handle;
6536 return &handle;
6539 static void
6540 pidgin_conversations_pre_uninit(void);
6542 void
6543 pidgin_conversations_init(void)
6545 void *handle = pidgin_conversations_get_handle();
6546 void *blist_handle = purple_blist_get_handle();
6548 e2ee_stock = g_hash_table_new_full(g_str_hash, g_str_equal,
6549 g_free, g_object_unref);
6551 /* Conversations */
6552 purple_prefs_add_none(PIDGIN_PREFS_ROOT "/conversations");
6553 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/use_smooth_scrolling", TRUE);
6554 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/close_on_tabs", TRUE);
6555 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/send_bold", FALSE);
6556 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/send_italic", FALSE);
6557 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/send_underline", FALSE);
6558 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/send_strike", FALSE);
6559 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/spellcheck", TRUE);
6560 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/show_incoming_formatting", TRUE);
6561 /* TODO: it's about *remote* smileys, not local ones */
6562 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/resize_custom_smileys", TRUE);
6563 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/custom_smileys_size", 96);
6564 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/minimum_entry_lines", 2);
6566 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/show_formatting_toolbar", TRUE);
6568 purple_prefs_add_string(PIDGIN_PREFS_ROOT "/conversations/placement", "last");
6569 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/placement_number", 1);
6570 purple_prefs_add_string(PIDGIN_PREFS_ROOT "/conversations/bgcolor", "");
6571 purple_prefs_add_string(PIDGIN_PREFS_ROOT "/conversations/fgcolor", "");
6572 purple_prefs_add_string(PIDGIN_PREFS_ROOT "/conversations/font_face", "");
6573 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/font_size", 3);
6574 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/tabs", TRUE);
6575 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/tab_side", GTK_POS_TOP);
6576 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/scrollback_lines", 4000);
6578 #ifdef _WIN32
6579 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/use_theme_font", TRUE);
6580 purple_prefs_add_string(PIDGIN_PREFS_ROOT "/conversations/custom_font", "");
6581 #endif
6583 /* Conversations -> Chat */
6584 purple_prefs_add_none(PIDGIN_PREFS_ROOT "/conversations/chat");
6585 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/chat/entry_height", 54);
6586 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/chat/userlist_width", 80);
6587 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/chat/x", 0);
6588 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/chat/y", 0);
6589 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/chat/width", 340);
6590 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/chat/height", 390);
6592 /* Conversations -> IM */
6593 purple_prefs_add_none(PIDGIN_PREFS_ROOT "/conversations/im");
6594 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/im/x", 0);
6595 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/im/y", 0);
6596 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/im/width", 340);
6597 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/im/height", 390);
6599 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/im/animate_buddy_icons", TRUE);
6601 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/im/entry_height", 54);
6602 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/im/show_buddy_icons", TRUE);
6604 purple_prefs_add_string(PIDGIN_PREFS_ROOT "/conversations/im/hide_new", "never");
6605 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/im/close_immediately", TRUE);
6607 #ifdef _WIN32
6608 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/win32/minimize_new_convs", FALSE);
6609 #endif
6611 /* Connect callbacks. */
6612 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/close_on_tabs",
6613 close_on_tabs_pref_cb, NULL);
6614 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/show_formatting_toolbar",
6615 show_formatting_toolbar_pref_cb, NULL);
6616 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/spellcheck",
6617 spellcheck_pref_cb, NULL);
6618 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/tab_side",
6619 tab_side_pref_cb, NULL);
6621 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/tabs",
6622 conv_placement_usetabs_cb, NULL);
6624 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/placement",
6625 conv_placement_pref_cb, NULL);
6626 purple_prefs_trigger_callback(PIDGIN_PREFS_ROOT "/conversations/placement");
6628 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/minimum_entry_lines",
6629 minimum_entry_lines_pref_cb, NULL);
6631 /* IM callbacks */
6632 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/im/animate_buddy_icons",
6633 animate_buddy_icons_pref_cb, NULL);
6634 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/im/show_buddy_icons",
6635 show_buddy_icons_pref_cb, NULL);
6636 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/blist/show_protocol_icons",
6637 show_protocol_icons_pref_cb, NULL);
6638 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/im/hide_new",
6639 hide_new_pref_cb, NULL);
6641 /**********************************************************************
6642 * Register signals
6643 **********************************************************************/
6644 purple_signal_register(handle, "conversation-dragging",
6645 purple_marshal_VOID__POINTER_POINTER, G_TYPE_NONE, 2,
6646 G_TYPE_POINTER, /* pointer to a (PidginConvWindow *) */
6647 G_TYPE_POINTER); /* pointer to a (PidginConvWindow *) */
6649 purple_signal_register(handle, "conversation-timestamp",
6650 #if SIZEOF_TIME_T == 4
6651 purple_marshal_POINTER__POINTER_INT_BOOLEAN,
6652 #elif SIZEOF_TIME_T == 8
6653 purple_marshal_POINTER__POINTER_INT64_BOOLEAN,
6654 #else
6655 #error Unkown size of time_t
6656 #endif
6657 G_TYPE_STRING, 3, PURPLE_TYPE_CONVERSATION,
6658 #if SIZEOF_TIME_T == 4
6659 G_TYPE_INT,
6660 #elif SIZEOF_TIME_T == 8
6661 G_TYPE_INT64,
6662 #else
6663 # error Unknown size of time_t
6664 #endif
6665 G_TYPE_BOOLEAN);
6667 purple_signal_register(handle, "displaying-im-msg",
6668 purple_marshal_BOOLEAN__POINTER_POINTER,
6669 G_TYPE_BOOLEAN, 2, PURPLE_TYPE_CONVERSATION, PURPLE_TYPE_MESSAGE);
6671 purple_signal_register(handle, "displayed-im-msg",
6672 purple_marshal_VOID__POINTER_POINTER, G_TYPE_NONE, 2,
6673 PURPLE_TYPE_CONVERSATION, PURPLE_TYPE_MESSAGE);
6675 purple_signal_register(handle, "displaying-chat-msg",
6676 purple_marshal_BOOLEAN__POINTER_POINTER,
6677 G_TYPE_BOOLEAN, 2, PURPLE_TYPE_CONVERSATION, PURPLE_TYPE_MESSAGE);
6679 purple_signal_register(handle, "displayed-chat-msg",
6680 purple_marshal_VOID__POINTER_POINTER, G_TYPE_NONE, 2,
6681 PURPLE_TYPE_CONVERSATION, PURPLE_TYPE_MESSAGE);
6683 purple_signal_register(handle, "conversation-switched",
6684 purple_marshal_VOID__POINTER, G_TYPE_NONE, 1,
6685 PURPLE_TYPE_CONVERSATION);
6687 purple_signal_register(handle, "conversation-hiding",
6688 purple_marshal_VOID__POINTER, G_TYPE_NONE, 1,
6689 G_TYPE_POINTER); /* (PidginConversation *) */
6691 purple_signal_register(handle, "conversation-displayed",
6692 purple_marshal_VOID__POINTER, G_TYPE_NONE, 1,
6693 G_TYPE_POINTER); /* (PidginConversation *) */
6695 purple_signal_register(handle, "chat-nick-autocomplete",
6696 purple_marshal_BOOLEAN__POINTER_BOOLEAN,
6697 G_TYPE_BOOLEAN, 1, PURPLE_TYPE_CONVERSATION);
6699 purple_signal_register(handle, "chat-nick-clicked",
6700 purple_marshal_BOOLEAN__POINTER_POINTER_UINT,
6701 G_TYPE_BOOLEAN, 3, PURPLE_TYPE_CONVERSATION,
6702 G_TYPE_STRING, G_TYPE_UINT);
6704 purple_signal_register(handle, "conversation-window-created",
6705 purple_marshal_VOID__POINTER, G_TYPE_NONE, 1,
6706 G_TYPE_POINTER); /* (PidginConvWindow *) */
6709 /**********************************************************************
6710 * Register commands
6711 **********************************************************************/
6712 purple_cmd_register("say", "S", PURPLE_CMD_P_DEFAULT,
6713 PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM, NULL,
6714 say_command_cb, _("say &lt;message&gt;: Send a message normally as if you weren't using a command."), NULL);
6715 purple_cmd_register("me", "S", PURPLE_CMD_P_DEFAULT,
6716 PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM, NULL,
6717 me_command_cb, _("me &lt;action&gt;: Send an IRC style action to a buddy or chat."), NULL);
6718 purple_cmd_register("debug", "w", PURPLE_CMD_P_DEFAULT,
6719 PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM, NULL,
6720 debug_command_cb, _("debug &lt;option&gt;: Send various debug information to the current conversation."), NULL);
6721 purple_cmd_register("clear", "", PURPLE_CMD_P_DEFAULT,
6722 PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM, NULL,
6723 clear_command_cb, _("clear: Clears the conversation scrollback."), NULL);
6724 purple_cmd_register("clearall", "", PURPLE_CMD_P_DEFAULT,
6725 PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM, NULL,
6726 clearall_command_cb, _("clear: Clears all conversation scrollbacks."), NULL);
6727 purple_cmd_register("help", "w", PURPLE_CMD_P_DEFAULT,
6728 PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM | PURPLE_CMD_FLAG_ALLOW_WRONG_ARGS, NULL,
6729 help_command_cb, _("help &lt;command&gt;: Help on a specific command."), NULL);
6731 /**********************************************************************
6732 * UI operations
6733 **********************************************************************/
6735 purple_signal_connect(purple_connections_get_handle(), "signed-on", handle,
6736 G_CALLBACK(account_signed_off_cb),
6737 GINT_TO_POINTER(PURPLE_CONVERSATION_ACCOUNT_ONLINE));
6738 purple_signal_connect(purple_connections_get_handle(), "signed-off", handle,
6739 G_CALLBACK(account_signed_off_cb),
6740 GINT_TO_POINTER(PURPLE_CONVERSATION_ACCOUNT_OFFLINE));
6741 purple_signal_connect(purple_connections_get_handle(), "signing-off", handle,
6742 G_CALLBACK(account_signing_off), NULL);
6744 purple_signal_connect(purple_conversations_get_handle(), "writing-im-msg",
6745 handle, G_CALLBACK(writing_msg), NULL);
6746 purple_signal_connect(purple_conversations_get_handle(), "writing-chat-msg",
6747 handle, G_CALLBACK(writing_msg), NULL);
6748 purple_signal_connect(purple_conversations_get_handle(), "received-im-msg",
6749 handle, G_CALLBACK(received_im_msg_cb), NULL);
6750 purple_signal_connect(purple_conversations_get_handle(), "cleared-message-history",
6751 handle, G_CALLBACK(clear_conversation_scrollback_cb), NULL);
6753 purple_signal_connect(purple_conversations_get_handle(), "deleting-chat-user",
6754 handle, G_CALLBACK(deleting_chat_user_cb), NULL);
6756 purple_conversations_set_ui_ops(&conversation_ui_ops);
6758 hidden_convwin = pidgin_conv_window_new();
6759 window_list = g_list_remove(window_list, hidden_convwin);
6761 purple_signal_connect(purple_accounts_get_handle(), "account-status-changed",
6762 handle, PURPLE_CALLBACK(account_status_changed_cb), NULL);
6764 purple_signal_connect_priority(purple_get_core(), "quitting", handle,
6765 PURPLE_CALLBACK(pidgin_conversations_pre_uninit), NULL, PURPLE_SIGNAL_PRIORITY_HIGHEST);
6767 /* Callbacks to update a conversation */
6768 purple_signal_connect(blist_handle, "blist-node-added", handle,
6769 G_CALLBACK(buddy_update_cb), NULL);
6770 purple_signal_connect(blist_handle, "blist-node-removed", handle,
6771 G_CALLBACK(buddy_update_cb), NULL);
6772 purple_signal_connect(blist_handle, "buddy-signed-on",
6773 handle, PURPLE_CALLBACK(update_buddy_sign), "on");
6774 purple_signal_connect(blist_handle, "buddy-signed-off",
6775 handle, PURPLE_CALLBACK(update_buddy_sign), "off");
6776 purple_signal_connect(blist_handle, "buddy-status-changed",
6777 handle, PURPLE_CALLBACK(update_buddy_status_changed), NULL);
6778 purple_signal_connect(blist_handle, "buddy-privacy-changed",
6779 handle, PURPLE_CALLBACK(update_buddy_privacy_changed), NULL);
6780 purple_signal_connect(blist_handle, "buddy-idle-changed",
6781 handle, PURPLE_CALLBACK(update_buddy_idle_changed), NULL);
6782 purple_signal_connect(blist_handle, "buddy-icon-changed",
6783 handle, PURPLE_CALLBACK(update_buddy_icon), NULL);
6784 purple_signal_connect(purple_conversations_get_handle(), "buddy-typing",
6785 handle, PURPLE_CALLBACK(update_buddy_typing), NULL);
6786 purple_signal_connect(purple_conversations_get_handle(), "buddy-typing-stopped",
6787 handle, PURPLE_CALLBACK(update_buddy_typing), NULL);
6788 purple_signal_connect(pidgin_conversations_get_handle(), "conversation-switched",
6789 handle, PURPLE_CALLBACK(update_conversation_switched), NULL);
6790 purple_signal_connect(purple_conversations_get_handle(), "chat-left", handle,
6791 PURPLE_CALLBACK(update_chat), NULL);
6792 purple_signal_connect(purple_conversations_get_handle(), "chat-joined", handle,
6793 PURPLE_CALLBACK(update_chat), NULL);
6794 purple_signal_connect(purple_conversations_get_handle(), "chat-topic-changed", handle,
6795 PURPLE_CALLBACK(update_chat_topic), NULL);
6796 purple_signal_connect_priority(purple_conversations_get_handle(), "conversation-updated", handle,
6797 PURPLE_CALLBACK(pidgin_conv_updated), NULL,
6798 PURPLE_SIGNAL_PRIORITY_LOWEST);
6799 purple_signal_connect(purple_conversations_get_handle(), "wrote-im-msg", handle,
6800 PURPLE_CALLBACK(wrote_msg_update_unseen_cb), NULL);
6801 purple_signal_connect(purple_conversations_get_handle(), "wrote-chat-msg", handle,
6802 PURPLE_CALLBACK(wrote_msg_update_unseen_cb), NULL);
6805 static void
6806 pidgin_conversations_pre_uninit(void)
6808 g_hash_table_destroy(e2ee_stock);
6809 e2ee_stock = NULL;
6812 /* Invalidate the first tab color set */
6813 static gboolean tab_color_fuse = TRUE;
6815 static void
6816 pidgin_conversations_set_tab_colors(void)
6818 /* Set default tab colors */
6819 GString *str = g_string_new(NULL);
6820 GtkSettings *settings = gtk_settings_get_default();
6821 GtkStyle *parent = gtk_rc_get_style_by_paths(settings, "tab-container.tab-label*", NULL, G_TYPE_NONE), *now;
6822 struct {
6823 const char *stylename;
6824 const char *labelname;
6825 const char *color;
6826 } styles[] = {
6827 {"pidgin_tab_label_typing_default", "tab-label-typing", "#4e9a06"},
6828 {"pidgin_tab_label_typed_default", "tab-label-typed", "#c4a000"},
6829 {"pidgin_tab_label_attention_default", "tab-label-attention", "#006aff"},
6830 {"pidgin_tab_label_unreadchat_default", "tab-label-unreadchat", "#cc0000"},
6831 {"pidgin_tab_label_event_default", "tab-label-event", "#888a85"},
6832 {NULL, NULL, NULL}
6834 int iter;
6836 if(tab_color_fuse) {
6837 tab_color_fuse = FALSE;
6838 return;
6841 for (iter = 0; styles[iter].stylename; iter++) {
6842 now = gtk_rc_get_style_by_paths(settings, styles[iter].labelname, NULL, G_TYPE_NONE);
6843 if (parent == now ||
6844 (parent && now && parent->rc_style == now->rc_style)) {
6845 GdkRGBA color;
6846 gchar *color_str;
6848 gdk_rgba_parse(&color, styles[iter].color);
6849 pidgin_style_adjust_contrast(gtk_widget_get_default_style(), &color);
6851 color_str = gdk_rgba_to_string(&color);
6852 g_string_append_printf(str, "style \"%s\" {\n"
6853 "fg[ACTIVE] = \"%s\"\n"
6854 "}\n"
6855 "widget \"*%s\" style \"%s\"\n",
6856 styles[iter].stylename,
6857 color_str,
6858 styles[iter].labelname, styles[iter].stylename);
6859 g_free(color_str);
6862 gtk_rc_parse_string(str->str);
6863 g_string_free(str, TRUE);
6864 gtk_rc_reset_styles(settings);
6867 void
6868 pidgin_conversations_uninit(void)
6870 purple_prefs_disconnect_by_handle(pidgin_conversations_get_handle());
6871 purple_signals_disconnect_by_handle(pidgin_conversations_get_handle());
6872 purple_signals_unregister_by_instance(pidgin_conversations_get_handle());
6875 /**************************************************************************
6876 * PidginConversation GBoxed code
6877 **************************************************************************/
6878 static PidginConversation *
6879 pidgin_conversation_ref(PidginConversation *gtkconv)
6881 g_return_val_if_fail(gtkconv != NULL, NULL);
6883 gtkconv->box_count++;
6885 return gtkconv;
6888 static void
6889 pidgin_conversation_unref(PidginConversation *gtkconv)
6891 g_return_if_fail(gtkconv != NULL);
6892 g_return_if_fail(gtkconv->box_count >= 0);
6894 if (!gtkconv->box_count--)
6895 pidgin_conv_destroy(gtkconv->active_conv);
6898 GType
6899 pidgin_conversation_get_type(void)
6901 static GType type = 0;
6903 if (type == 0) {
6904 type = g_boxed_type_register_static("PidginConversation",
6905 (GBoxedCopyFunc)pidgin_conversation_ref,
6906 (GBoxedFreeFunc)pidgin_conversation_unref);
6909 return type;
6927 /* down here is where gtkconvwin.c ought to start. except they share like every freaking function,
6928 * and touch each others' private members all day long */
6930 /* pidgin
6932 * Pidgin is the legal property of its developers, whose names are too numerous
6933 * to list here. Please refer to the COPYRIGHT file distributed with this
6934 * source distribution.
6936 * This program is free software; you can redistribute it and/or modify
6937 * it under the terms of the GNU General Public License as published by
6938 * the Free Software Foundation; either version 2 of the License, or
6939 * (at your option) any later version.
6941 * This program is distributed in the hope that it will be useful,
6942 * but WITHOUT ANY WARRANTY; without even the implied warranty of
6943 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
6944 * GNU General Public License for more details.
6946 * You should have received a copy of the GNU General Public License
6947 * along with this program; if not, write to the Free Software
6948 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA
6951 #include "internal.h"
6952 #include "pidgin.h"
6955 #include <gdk/gdkkeysyms.h>
6957 #include "account.h"
6958 #include "cmds.h"
6959 #include "debug.h"
6960 #include "log.h"
6961 #include "notify.h"
6962 #include "protocol.h"
6963 #include "request.h"
6964 #include "util.h"
6966 #include "gtkdnd-hints.h"
6967 #include "gtkblist.h"
6968 #include "gtkconv.h"
6969 #include "gtkdialogs.h"
6970 #include "gtkmenutray.h"
6971 #include "gtkpounce.h"
6972 #include "gtkprefs.h"
6973 #include "gtkprivacy.h"
6974 #include "gtkutils.h"
6975 #include "pidginstock.h"
6977 static void
6978 do_close(GtkWidget *w, int resp, PidginConvWindow *win)
6980 gtk_widget_destroy(warn_close_dialog);
6981 warn_close_dialog = NULL;
6983 if (resp == GTK_RESPONSE_OK)
6984 pidgin_conv_window_destroy(win);
6987 static void
6988 build_warn_close_dialog(PidginConvWindow *gtkwin)
6990 GtkWidget *label, *vbox, *hbox, *img;
6992 g_return_if_fail(warn_close_dialog == NULL);
6994 warn_close_dialog = gtk_dialog_new_with_buttons(_("Confirm close"),
6995 GTK_WINDOW(gtkwin->window), GTK_DIALOG_MODAL,
6996 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
6997 GTK_STOCK_CLOSE, GTK_RESPONSE_OK, NULL);
6999 gtk_dialog_set_default_response(GTK_DIALOG(warn_close_dialog),
7000 GTK_RESPONSE_OK);
7002 gtk_container_set_border_width(GTK_CONTAINER(warn_close_dialog),
7004 gtk_window_set_resizable(GTK_WINDOW(warn_close_dialog), FALSE);
7006 /* Setup the outside spacing. */
7007 vbox = gtk_dialog_get_content_area(GTK_DIALOG(warn_close_dialog));
7009 gtk_box_set_spacing(GTK_BOX(vbox), 12);
7010 gtk_container_set_border_width(GTK_CONTAINER(vbox), 6);
7012 img = gtk_image_new_from_icon_name("dialog-warning",
7013 GTK_ICON_SIZE_DIALOG);
7015 /* Setup the inner hbox and put the dialog's icon in it. */
7016 hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 12);
7017 gtk_container_add(GTK_CONTAINER(vbox), hbox);
7018 gtk_box_pack_start(GTK_BOX(hbox), img, FALSE, FALSE, 0);
7019 gtk_widget_set_halign(img, GTK_ALIGN_START);
7020 gtk_widget_set_valign(img, GTK_ALIGN_START);
7022 /* Setup the right vbox. */
7023 vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12);
7024 gtk_container_add(GTK_CONTAINER(hbox), vbox);
7026 label = gtk_label_new(_("You have unread messages. Are you sure you want to close the window?"));
7027 gtk_widget_set_size_request(label, 350, -1);
7028 gtk_label_set_line_wrap(GTK_LABEL(label), TRUE);
7029 gtk_label_set_xalign(GTK_LABEL(label), 0);
7030 gtk_label_set_yalign(GTK_LABEL(label), 0);
7031 gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, FALSE, 0);
7033 /* Connect the signals. */
7034 g_signal_connect(G_OBJECT(warn_close_dialog), "response",
7035 G_CALLBACK(do_close), gtkwin);
7039 /**************************************************************************
7040 * Callbacks
7041 **************************************************************************/
7043 static gboolean
7044 close_win_cb(GtkWidget *w, GdkEventAny *e, gpointer d)
7046 PidginConvWindow *win = d;
7047 GList *l;
7049 /* If there are unread messages then show a warning dialog */
7050 for (l = pidgin_conv_window_get_gtkconvs(win);
7051 l != NULL; l = l->next)
7053 PidginConversation *gtkconv = l->data;
7054 if (PURPLE_IS_IM_CONVERSATION(gtkconv->active_conv) &&
7055 gtkconv->unseen_state >= PIDGIN_UNSEEN_TEXT)
7057 build_warn_close_dialog(win);
7058 gtk_widget_show_all(warn_close_dialog);
7060 return TRUE;
7064 pidgin_conv_window_destroy(win);
7066 return TRUE;
7069 static void
7070 conv_set_unseen(PurpleConversation *conv, PidginUnseenState state)
7072 int unseen_count = 0;
7073 PidginUnseenState unseen_state = PIDGIN_UNSEEN_NONE;
7075 if(g_object_get_data(G_OBJECT(conv), "unseen-count"))
7076 unseen_count = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(conv), "unseen-count"));
7078 if(g_object_get_data(G_OBJECT(conv), "unseen-state"))
7079 unseen_state = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(conv), "unseen-state"));
7081 if (state == PIDGIN_UNSEEN_NONE)
7083 unseen_count = 0;
7084 unseen_state = PIDGIN_UNSEEN_NONE;
7086 else
7088 if (state >= PIDGIN_UNSEEN_TEXT)
7089 unseen_count++;
7091 if (state > unseen_state)
7092 unseen_state = state;
7095 g_object_set_data(G_OBJECT(conv), "unseen-count", GINT_TO_POINTER(unseen_count));
7096 g_object_set_data(G_OBJECT(conv), "unseen-state", GINT_TO_POINTER(unseen_state));
7098 purple_conversation_update(conv, PURPLE_CONVERSATION_UPDATE_UNSEEN);
7101 static void
7102 gtkconv_set_unseen(PidginConversation *gtkconv, PidginUnseenState state)
7104 if (state == PIDGIN_UNSEEN_NONE)
7106 gtkconv->unseen_count = 0;
7107 gtkconv->unseen_state = PIDGIN_UNSEEN_NONE;
7109 else
7111 if (state >= PIDGIN_UNSEEN_TEXT)
7112 gtkconv->unseen_count++;
7114 if (state > gtkconv->unseen_state)
7115 gtkconv->unseen_state = state;
7118 g_object_set_data(G_OBJECT(gtkconv->active_conv), "unseen-count", GINT_TO_POINTER(gtkconv->unseen_count));
7119 g_object_set_data(G_OBJECT(gtkconv->active_conv), "unseen-state", GINT_TO_POINTER(gtkconv->unseen_state));
7121 purple_conversation_update(gtkconv->active_conv, PURPLE_CONVERSATION_UPDATE_UNSEEN);
7125 * When a conversation window is focused, we know the user
7126 * has looked at it so we know there are no longer unseen
7127 * messages.
7129 static gboolean
7130 focus_win_cb(GtkWidget *w, GdkEventFocus *e, gpointer d)
7132 PidginConvWindow *win = d;
7133 PidginConversation *gtkconv = pidgin_conv_window_get_active_gtkconv(win);
7135 if (gtkconv)
7136 gtkconv_set_unseen(gtkconv, PIDGIN_UNSEEN_NONE);
7138 return FALSE;
7141 static void
7142 notebook_init_grab(PidginConvWindow *gtkwin, GtkWidget *widget, GdkEvent *event)
7144 static GdkCursor *cursor = NULL;
7145 GdkDevice *device;
7147 gtkwin->in_drag = TRUE;
7149 if (gtkwin->drag_leave_signal) {
7150 g_signal_handler_disconnect(G_OBJECT(widget),
7151 gtkwin->drag_leave_signal);
7152 gtkwin->drag_leave_signal = 0;
7155 if (cursor == NULL) {
7156 GdkDisplay *display = gtk_widget_get_display(gtkwin->notebook);
7157 cursor = gdk_cursor_new_for_display(display, GDK_FLEUR);
7160 /* Grab the pointer */
7161 gtk_grab_add(gtkwin->notebook);
7162 device = gdk_event_get_device(event);
7163 if (!gdk_display_device_is_grabbed(gdk_device_get_display(device), device))
7164 gdk_device_grab(device, gtk_widget_get_window(gtkwin->notebook),
7165 GDK_OWNERSHIP_WINDOW, FALSE,
7166 GDK_BUTTON1_MOTION_MASK | GDK_BUTTON_RELEASE_MASK,
7167 cursor, gdk_event_get_time(event));
7170 static gboolean
7171 notebook_motion_cb(GtkWidget *widget, GdkEventButton *e, PidginConvWindow *win)
7175 * Make sure the user moved the mouse far enough for the
7176 * drag to be initiated.
7178 if (win->in_predrag) {
7179 if (e->x_root < win->drag_min_x ||
7180 e->x_root >= win->drag_max_x ||
7181 e->y_root < win->drag_min_y ||
7182 e->y_root >= win->drag_max_y) {
7184 win->in_predrag = FALSE;
7185 notebook_init_grab(win, widget, (GdkEvent *)e);
7188 else { /* Otherwise, draw the arrows. */
7189 PidginConvWindow *dest_win;
7190 GtkNotebook *dest_notebook;
7191 GtkWidget *tab;
7192 gint page_num;
7193 gboolean horiz_tabs = FALSE;
7194 gboolean to_right = FALSE;
7196 /* Get the window that the cursor is over. */
7197 dest_win = pidgin_conv_window_get_at_event((GdkEvent *)e);
7199 if (dest_win == NULL) {
7200 pidgin_dnd_hints_hide_all();
7202 return TRUE;
7205 dest_notebook = GTK_NOTEBOOK(dest_win->notebook);
7207 if (gtk_notebook_get_show_tabs(dest_notebook)) {
7208 page_num = pidgin_conv_get_tab_at_xy(dest_win,
7209 e->x_root, e->y_root, &to_right);
7210 to_right = to_right && (win != dest_win);
7211 tab = pidgin_conv_window_get_gtkconv_at_index(dest_win, page_num)->tabby;
7212 } else {
7213 page_num = 0;
7214 to_right = pidgin_conv_xy_to_right_infopane(dest_win, e->x_root, e->y_root);
7215 tab = pidgin_conv_window_get_gtkconv_at_index(dest_win, page_num)->infopane_hbox;
7218 if (gtk_notebook_get_tab_pos(dest_notebook) == GTK_POS_TOP ||
7219 gtk_notebook_get_tab_pos(dest_notebook) == GTK_POS_BOTTOM) {
7220 horiz_tabs = TRUE;
7223 if (gtk_notebook_get_show_tabs(dest_notebook) == FALSE && win == dest_win)
7225 /* dragging a tab from a single-tabbed window over its own window */
7226 pidgin_dnd_hints_hide_all();
7227 return TRUE;
7228 } else if (horiz_tabs) {
7229 if (((gpointer)win == (gpointer)dest_win && win->drag_tab < page_num) || to_right) {
7230 pidgin_dnd_hints_show_relative(HINT_ARROW_DOWN, tab, HINT_POSITION_RIGHT, HINT_POSITION_TOP);
7231 pidgin_dnd_hints_show_relative(HINT_ARROW_UP, tab, HINT_POSITION_RIGHT, HINT_POSITION_BOTTOM);
7232 } else {
7233 pidgin_dnd_hints_show_relative(HINT_ARROW_DOWN, tab, HINT_POSITION_LEFT, HINT_POSITION_TOP);
7234 pidgin_dnd_hints_show_relative(HINT_ARROW_UP, tab, HINT_POSITION_LEFT, HINT_POSITION_BOTTOM);
7236 } else {
7237 if (((gpointer)win == (gpointer)dest_win && win->drag_tab < page_num) || to_right) {
7238 pidgin_dnd_hints_show_relative(HINT_ARROW_RIGHT, tab, HINT_POSITION_LEFT, HINT_POSITION_BOTTOM);
7239 pidgin_dnd_hints_show_relative(HINT_ARROW_LEFT, tab, HINT_POSITION_RIGHT, HINT_POSITION_BOTTOM);
7240 } else {
7241 pidgin_dnd_hints_show_relative(HINT_ARROW_RIGHT, tab, HINT_POSITION_LEFT, HINT_POSITION_TOP);
7242 pidgin_dnd_hints_show_relative(HINT_ARROW_LEFT, tab, HINT_POSITION_RIGHT, HINT_POSITION_TOP);
7247 return TRUE;
7250 static gboolean
7251 notebook_leave_cb(GtkWidget *widget, GdkEventCrossing *e, PidginConvWindow *win)
7253 if (win->in_drag)
7254 return FALSE;
7256 if (e->x_root < win->drag_min_x ||
7257 e->x_root >= win->drag_max_x ||
7258 e->y_root < win->drag_min_y ||
7259 e->y_root >= win->drag_max_y) {
7261 win->in_predrag = FALSE;
7262 notebook_init_grab(win, widget, (GdkEvent *)e);
7265 return TRUE;
7269 * THANK YOU GALEON!
7272 static gboolean
7273 infopane_press_cb(GtkWidget *widget, GdkEventButton *e, PidginConversation *gtkconv)
7275 if (e->type == GDK_2BUTTON_PRESS && e->button == GDK_BUTTON_PRIMARY) {
7276 if (infopane_entry_activate(gtkconv))
7277 return TRUE;
7280 if (e->type != GDK_BUTTON_PRESS)
7281 return FALSE;
7283 if (e->button == GDK_BUTTON_PRIMARY) {
7284 int nb_x, nb_y;
7285 GtkAllocation allocation;
7287 gtk_widget_get_allocation(gtkconv->infopane_hbox, &allocation);
7289 if (gtkconv->win->in_drag)
7290 return TRUE;
7292 gtkconv->win->in_predrag = TRUE;
7293 gtkconv->win->drag_tab = gtk_notebook_page_num(GTK_NOTEBOOK(gtkconv->win->notebook), gtkconv->tab_cont);
7295 gdk_window_get_origin(gtk_widget_get_window(gtkconv->infopane_hbox), &nb_x, &nb_y);
7297 gtkconv->win->drag_min_x = allocation.x + nb_x;
7298 gtkconv->win->drag_min_y = allocation.y + nb_y;
7299 gtkconv->win->drag_max_x = allocation.width + gtkconv->win->drag_min_x;
7300 gtkconv->win->drag_max_y = allocation.height + gtkconv->win->drag_min_y;
7302 gtkconv->win->drag_motion_signal = g_signal_connect(G_OBJECT(gtkconv->win->notebook), "motion_notify_event",
7303 G_CALLBACK(notebook_motion_cb), gtkconv->win);
7304 gtkconv->win->drag_leave_signal = g_signal_connect(G_OBJECT(gtkconv->win->notebook), "leave_notify_event",
7305 G_CALLBACK(notebook_leave_cb), gtkconv->win);
7306 return FALSE;
7309 if (gdk_event_triggers_context_menu((GdkEvent *)e)) {
7310 /* Right click was pressed. Popup the context menu. */
7311 GtkWidget *menu = gtk_menu_new(), *sub;
7312 gboolean populated = populate_menu_with_options(menu, gtkconv, TRUE);
7314 sub = gtk_menu_item_get_submenu(GTK_MENU_ITEM(gtkconv->win->menu->send_to));
7315 if (sub && gtk_widget_is_sensitive(gtkconv->win->menu->send_to)) {
7316 GtkWidget *item = gtk_menu_item_new_with_mnemonic(_("S_end To"));
7317 if (populated)
7318 pidgin_separator(menu);
7319 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
7320 gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), sub);
7321 gtk_widget_show(item);
7322 gtk_widget_show_all(sub);
7323 } else if (!populated) {
7324 gtk_widget_destroy(menu);
7325 return FALSE;
7328 gtk_widget_show_all(menu);
7329 gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent *)e);
7330 return TRUE;
7332 return FALSE;
7335 static gboolean
7336 notebook_press_cb(GtkWidget *widget, GdkEventButton *e, PidginConvWindow *win)
7338 gint nb_x, nb_y;
7339 int tab_clicked;
7340 GtkWidget *page;
7341 GtkWidget *tab;
7342 GtkAllocation allocation;
7344 if (e->button == GDK_BUTTON_MIDDLE && e->type == GDK_BUTTON_PRESS) {
7345 PidginConversation *gtkconv;
7346 tab_clicked = pidgin_conv_get_tab_at_xy(win, e->x_root, e->y_root, NULL);
7348 if (tab_clicked == -1)
7349 return FALSE;
7351 gtkconv = pidgin_conv_window_get_gtkconv_at_index(win, tab_clicked);
7352 close_conv_cb(NULL, gtkconv);
7353 return TRUE;
7357 if (e->button != GDK_BUTTON_PRIMARY || e->type != GDK_BUTTON_PRESS)
7358 return FALSE;
7361 if (win->in_drag) {
7362 purple_debug(PURPLE_DEBUG_WARNING, "gtkconv",
7363 "Already in the middle of a window drag at tab_press_cb\n");
7364 return TRUE;
7368 * Make sure a tab was actually clicked. The arrow buttons
7369 * mess things up.
7371 tab_clicked = pidgin_conv_get_tab_at_xy(win, e->x_root, e->y_root, NULL);
7373 if (tab_clicked == -1)
7374 return FALSE;
7377 * Get the relative position of the press event, with regards to
7378 * the position of the notebook.
7380 gdk_window_get_origin(gtk_widget_get_window(win->notebook), &nb_x, &nb_y);
7382 /* Reset the min/max x/y */
7383 win->drag_min_x = 0;
7384 win->drag_min_y = 0;
7385 win->drag_max_x = 0;
7386 win->drag_max_y = 0;
7388 /* Find out which tab was dragged. */
7389 page = gtk_notebook_get_nth_page(GTK_NOTEBOOK(win->notebook), tab_clicked);
7390 tab = gtk_notebook_get_tab_label(GTK_NOTEBOOK(win->notebook), page);
7392 gtk_widget_get_allocation(tab, &allocation);
7394 win->drag_min_x = allocation.x + nb_x;
7395 win->drag_min_y = allocation.y + nb_y;
7396 win->drag_max_x = allocation.width + win->drag_min_x;
7397 win->drag_max_y = allocation.height + win->drag_min_y;
7399 /* Make sure the click occurred in the tab. */
7400 if (e->x_root < win->drag_min_x ||
7401 e->x_root >= win->drag_max_x ||
7402 e->y_root < win->drag_min_y ||
7403 e->y_root >= win->drag_max_y) {
7405 return FALSE;
7408 win->in_predrag = TRUE;
7409 win->drag_tab = tab_clicked;
7411 /* Connect the new motion signals. */
7412 win->drag_motion_signal =
7413 g_signal_connect(G_OBJECT(widget), "motion_notify_event",
7414 G_CALLBACK(notebook_motion_cb), win);
7416 win->drag_leave_signal =
7417 g_signal_connect(G_OBJECT(widget), "leave_notify_event",
7418 G_CALLBACK(notebook_leave_cb), win);
7420 return FALSE;
7423 static gboolean
7424 notebook_release_cb(GtkWidget *widget, GdkEventButton *e, PidginConvWindow *win)
7426 PidginConvWindow *dest_win;
7427 GtkNotebook *dest_notebook;
7428 PidginConversation *active_gtkconv;
7429 PidginConversation *gtkconv;
7430 gint dest_page_num = 0;
7431 gboolean new_window = FALSE;
7432 gboolean to_right = FALSE;
7433 GdkDevice *device;
7436 * Don't check to make sure that the event's window matches the
7437 * widget's, because we may be getting an event passed on from the
7438 * close button.
7440 if (e->button != GDK_BUTTON_PRIMARY && e->type != GDK_BUTTON_RELEASE)
7441 return FALSE;
7443 device = gdk_event_get_device((GdkEvent *)e);
7444 if (gdk_display_device_is_grabbed(gdk_device_get_display(device), device)) {
7445 gdk_device_ungrab(device, gdk_event_get_time((GdkEvent *)e));
7446 gtk_grab_remove(widget);
7449 if (!win->in_predrag && !win->in_drag)
7450 return FALSE;
7452 /* Disconnect the motion signal. */
7453 if (win->drag_motion_signal) {
7454 g_signal_handler_disconnect(G_OBJECT(widget),
7455 win->drag_motion_signal);
7457 win->drag_motion_signal = 0;
7461 * If we're in a pre-drag, we'll also need to disconnect the leave
7462 * signal.
7464 if (win->in_predrag) {
7465 win->in_predrag = FALSE;
7467 if (win->drag_leave_signal) {
7468 g_signal_handler_disconnect(G_OBJECT(widget),
7469 win->drag_leave_signal);
7471 win->drag_leave_signal = 0;
7475 /* If we're not in drag... */
7476 /* We're perfectly normal people! */
7477 if (!win->in_drag)
7478 return FALSE;
7480 win->in_drag = FALSE;
7482 pidgin_dnd_hints_hide_all();
7484 dest_win = pidgin_conv_window_get_at_event((GdkEvent *)e);
7486 active_gtkconv = pidgin_conv_window_get_active_gtkconv(win);
7488 if (dest_win == NULL) {
7489 /* If the current window doesn't have any other conversations,
7490 * there isn't much point transferring the conv to a new window. */
7491 if (pidgin_conv_window_get_gtkconv_count(win) > 1) {
7492 /* Make a new window to stick this to. */
7493 dest_win = pidgin_conv_window_new();
7494 new_window = TRUE;
7498 if (dest_win == NULL)
7499 return FALSE;
7501 purple_signal_emit(pidgin_conversations_get_handle(),
7502 "conversation-dragging", win, dest_win);
7504 /* Get the destination page number. */
7505 if (!new_window) {
7506 dest_notebook = GTK_NOTEBOOK(dest_win->notebook);
7507 if (gtk_notebook_get_show_tabs(dest_notebook)) {
7508 dest_page_num = pidgin_conv_get_tab_at_xy(dest_win,
7509 e->x_root, e->y_root, &to_right);
7510 } else {
7511 dest_page_num = 0;
7512 to_right = pidgin_conv_xy_to_right_infopane(dest_win, e->x_root, e->y_root);
7516 gtkconv = pidgin_conv_window_get_gtkconv_at_index(win, win->drag_tab);
7518 if (win == dest_win) {
7519 gtk_notebook_reorder_child(GTK_NOTEBOOK(win->notebook), gtkconv->tab_cont, dest_page_num);
7520 } else {
7521 pidgin_conv_window_remove_gtkconv(win, gtkconv);
7522 pidgin_conv_window_add_gtkconv(dest_win, gtkconv);
7523 gtk_notebook_reorder_child(GTK_NOTEBOOK(dest_win->notebook), gtkconv->tab_cont, dest_page_num + to_right);
7524 pidgin_conv_window_switch_gtkconv(dest_win, gtkconv);
7525 if (new_window) {
7526 gint win_width, win_height;
7528 gtk_window_get_size(GTK_WINDOW(dest_win->window),
7529 &win_width, &win_height);
7530 #ifdef _WIN32 /* only override window manager placement on Windows */
7531 gtk_window_move(GTK_WINDOW(dest_win->window),
7532 e->x_root - (win_width / 2),
7533 e->y_root - (win_height / 2));
7534 #endif
7536 pidgin_conv_window_show(dest_win);
7540 gtk_widget_grab_focus(active_gtkconv->editor);
7542 return TRUE;
7546 static void
7547 before_switch_conv_cb(GtkNotebook *notebook, GtkWidget *page, gint page_num,
7548 gpointer user_data)
7550 PidginConvWindow *win;
7551 PurpleConversation *conv;
7552 PidginConversation *gtkconv;
7554 win = user_data;
7555 conv = pidgin_conv_window_get_active_conversation(win);
7557 g_return_if_fail(conv != NULL);
7559 if (!PURPLE_IS_IM_CONVERSATION(conv))
7560 return;
7562 gtkconv = PIDGIN_CONVERSATION(conv);
7564 if (gtkconv->u.im->typing_timer != 0) {
7565 g_source_remove(gtkconv->u.im->typing_timer);
7566 gtkconv->u.im->typing_timer = 0;
7569 stop_anim(NULL, gtkconv);
7572 static void
7573 close_window(GtkWidget *w, PidginConvWindow *win)
7575 close_win_cb(w, NULL, win);
7578 static void
7579 detach_tab_cb(GtkWidget *w, PidginConvWindow *win)
7581 PidginConvWindow *new_window;
7582 PidginConversation *gtkconv;
7584 gtkconv = win->clicked_tab;
7586 if (!gtkconv)
7587 return;
7589 /* Nothing to do if there's only one tab in the window */
7590 if (pidgin_conv_window_get_gtkconv_count(win) == 1)
7591 return;
7593 pidgin_conv_window_remove_gtkconv(win, gtkconv);
7595 new_window = pidgin_conv_window_new();
7596 pidgin_conv_window_add_gtkconv(new_window, gtkconv);
7597 pidgin_conv_window_show(new_window);
7600 static void
7601 close_others_cb(GtkWidget *w, PidginConvWindow *win)
7603 GList *iter;
7604 PidginConversation *gtkconv;
7606 gtkconv = win->clicked_tab;
7608 if (!gtkconv)
7609 return;
7611 for (iter = pidgin_conv_window_get_gtkconvs(win); iter; )
7613 PidginConversation *gconv = iter->data;
7614 iter = iter->next;
7616 if (gconv != gtkconv)
7618 close_conv_cb(NULL, gconv);
7623 static void
7624 close_tab_cb(GtkWidget *w, PidginConvWindow *win)
7626 PidginConversation *gtkconv;
7628 gtkconv = win->clicked_tab;
7630 if (gtkconv)
7631 close_conv_cb(NULL, gtkconv);
7634 static void
7635 notebook_menu_switch_cb(GtkWidget *item, GtkWidget *child)
7637 GtkNotebook *notebook;
7638 int index;
7640 notebook = GTK_NOTEBOOK(gtk_widget_get_parent(child));
7641 index = gtk_notebook_page_num(notebook, child);
7642 gtk_notebook_set_current_page(notebook, index);
7645 static void
7646 notebook_menu_update_label_cb(GtkWidget *child, GParamSpec *pspec,
7647 GtkNotebook *notebook)
7649 GtkWidget *item;
7650 GtkWidget *label;
7652 item = g_object_get_data(G_OBJECT(child), "popup-menu-item");
7653 label = gtk_bin_get_child(GTK_BIN(item));
7654 if (label)
7655 gtk_container_remove(GTK_CONTAINER(item), label);
7657 label = gtk_notebook_get_menu_label(notebook, child);
7658 if (label) {
7659 gtk_widget_show(label);
7660 gtk_container_add(GTK_CONTAINER(item), label);
7661 gtk_widget_show(item);
7662 } else {
7663 gtk_widget_hide(item);
7667 static void
7668 notebook_add_tab_to_menu_cb(GtkNotebook *notebook, GtkWidget *child,
7669 guint page_num, PidginConvWindow *win)
7671 GtkWidget *item;
7672 GtkWidget *label;
7674 item = gtk_menu_item_new();
7675 label = gtk_notebook_get_menu_label(notebook, child);
7676 if (label) {
7677 gtk_widget_show(label);
7678 gtk_container_add(GTK_CONTAINER(item), label);
7679 gtk_widget_show(item);
7682 g_signal_connect(child, "child-notify::menu-label",
7683 G_CALLBACK(notebook_menu_update_label_cb), notebook);
7684 g_signal_connect(item, "activate",
7685 G_CALLBACK(notebook_menu_switch_cb), child);
7686 g_object_set_data(G_OBJECT(child), "popup-menu-item", item);
7688 gtk_menu_shell_insert(GTK_MENU_SHELL(win->notebook_menu), item, page_num);
7691 static void
7692 notebook_remove_tab_from_menu_cb(GtkNotebook *notebook, GtkWidget *child,
7693 guint page_num, PidginConvWindow *win)
7695 GtkWidget *item;
7697 /* Disconnecting the "child-notify::menu-label" signal. */
7698 g_signal_handlers_disconnect_by_data(child, notebook);
7700 item = g_object_get_data(G_OBJECT(child), "popup-menu-item");
7701 gtk_container_remove(GTK_CONTAINER(win->notebook_menu), item);
7705 static void
7706 notebook_reorder_tab_in_menu_cb(GtkNotebook *notebook, GtkWidget *child,
7707 guint page_num, PidginConvWindow *win)
7709 GtkWidget *item;
7711 item = g_object_get_data(G_OBJECT(child), "popup-menu-item");
7712 gtk_menu_reorder_child(GTK_MENU(win->notebook_menu), item, page_num);
7715 static gboolean
7716 notebook_right_click_menu_cb(GtkNotebook *notebook, GdkEventButton *event,
7717 PidginConvWindow *win)
7719 GtkWidget *menu;
7720 PidginConversation *gtkconv;
7722 if (!gdk_event_triggers_context_menu((GdkEvent *)event))
7723 return FALSE;
7725 gtkconv = pidgin_conv_window_get_gtkconv_at_index(win,
7726 pidgin_conv_get_tab_at_xy(win, event->x_root, event->y_root, NULL));
7728 win->clicked_tab = gtkconv;
7730 menu = win->notebook_menu;
7732 gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent *)event);
7734 return TRUE;
7737 static void
7738 remove_edit_entry(PidginConversation *gtkconv, GtkWidget *entry)
7740 g_signal_handlers_disconnect_matched(G_OBJECT(entry), G_SIGNAL_MATCH_DATA,
7741 0, 0, NULL, NULL, gtkconv);
7742 gtk_widget_show(gtkconv->infopane);
7743 gtk_widget_grab_focus(gtkconv->editor);
7744 gtk_widget_destroy(entry);
7747 static gboolean
7748 alias_focus_cb(GtkWidget *widget, GdkEventFocus *event, gpointer user_data)
7750 remove_edit_entry(user_data, widget);
7751 return FALSE;
7754 static gboolean
7755 alias_key_press_cb(GtkWidget *widget, GdkEventKey *event, gpointer user_data)
7757 if (event->keyval == GDK_KEY_Escape) {
7758 remove_edit_entry(user_data, widget);
7759 return TRUE;
7761 return FALSE;
7764 static void
7765 alias_cb(GtkEntry *entry, gpointer user_data)
7767 PidginConversation *gtkconv;
7768 PurpleConversation *conv;
7769 PurpleAccount *account;
7770 const char *name;
7772 gtkconv = (PidginConversation *)user_data;
7773 if (gtkconv == NULL) {
7774 return;
7776 conv = gtkconv->active_conv;
7777 account = purple_conversation_get_account(conv);
7778 name = purple_conversation_get_name(conv);
7780 if (PURPLE_IS_IM_CONVERSATION(conv)) {
7781 PurpleBuddy *buddy;
7782 buddy = purple_blist_find_buddy(account, name);
7783 if (buddy != NULL) {
7784 purple_buddy_set_local_alias(buddy, gtk_entry_get_text(entry));
7786 purple_serv_alias_buddy(buddy);
7787 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
7788 gtk_entry_set_text(GTK_ENTRY(gtkconv->u.chat->topic_text), gtk_entry_get_text(entry));
7789 topic_callback(NULL, gtkconv);
7791 remove_edit_entry(user_data, GTK_WIDGET(entry));
7794 static gboolean
7795 infopane_entry_activate(PidginConversation *gtkconv)
7797 GtkWidget *entry = NULL;
7798 PurpleConversation *conv = gtkconv->active_conv;
7799 const char *text = NULL;
7801 if (!gtk_widget_get_visible(gtkconv->infopane)) {
7802 /* There's already an entry for alias. Let's not create another one. */
7803 return FALSE;
7806 if (!purple_account_is_connected(purple_conversation_get_account(gtkconv->active_conv))) {
7807 /* Do not allow aliasing someone on a disconnected account. */
7808 return FALSE;
7811 if (PURPLE_IS_IM_CONVERSATION(conv)) {
7812 PurpleBuddy *buddy = purple_blist_find_buddy(purple_conversation_get_account(gtkconv->active_conv), purple_conversation_get_name(gtkconv->active_conv));
7813 if (!buddy)
7814 /* This buddy isn't in your buddy list, so we can't alias him */
7815 return FALSE;
7817 text = purple_buddy_get_contact_alias(buddy);
7818 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
7819 PurpleConnection *gc;
7820 PurpleProtocol *protocol = NULL;
7822 gc = purple_conversation_get_connection(conv);
7823 if (gc != NULL)
7824 protocol = purple_connection_get_protocol(gc);
7825 if (protocol && !PURPLE_PROTOCOL_IMPLEMENTS(protocol, CHAT, set_topic))
7826 /* This protocol doesn't support setting the chat room topic */
7827 return FALSE;
7829 text = purple_chat_conversation_get_topic(PURPLE_CHAT_CONVERSATION(conv));
7832 /* alias label */
7833 entry = gtk_entry_new();
7834 gtk_entry_set_has_frame(GTK_ENTRY(entry), FALSE);
7835 gtk_entry_set_width_chars(GTK_ENTRY(entry), 10);
7836 gtk_entry_set_alignment(GTK_ENTRY(entry), 0.5);
7838 gtk_box_pack_start(GTK_BOX(gtkconv->infopane_hbox), entry, TRUE, TRUE, 0);
7839 /* after the tab label */
7840 gtk_box_reorder_child(GTK_BOX(gtkconv->infopane_hbox), entry, 0);
7842 g_signal_connect(G_OBJECT(entry), "activate", G_CALLBACK(alias_cb), gtkconv);
7843 g_signal_connect(G_OBJECT(entry), "focus-out-event", G_CALLBACK(alias_focus_cb), gtkconv);
7844 g_signal_connect(G_OBJECT(entry), "key-press-event", G_CALLBACK(alias_key_press_cb), gtkconv);
7846 if (text != NULL)
7847 gtk_entry_set_text(GTK_ENTRY(entry), text);
7848 gtk_widget_show(entry);
7849 gtk_widget_hide(gtkconv->infopane);
7850 gtk_widget_grab_focus(entry);
7852 return TRUE;
7855 static gboolean
7856 window_keypress_cb(GtkWidget *widget, GdkEventKey *event, PidginConvWindow *win)
7858 PidginConversation *gtkconv = pidgin_conv_window_get_active_gtkconv(win);
7860 return conv_keypress_common(gtkconv, event);
7863 static void
7864 switch_conv_cb(GtkNotebook *notebook, GtkWidget *page, gint page_num,
7865 gpointer user_data)
7867 PidginConvWindow *win;
7868 PurpleConversation *conv;
7869 PidginConversation *gtkconv;
7870 const char *sound_method;
7872 win = user_data;
7873 gtkconv = pidgin_conv_window_get_gtkconv_at_index(win, page_num);
7874 conv = gtkconv->active_conv;
7876 g_return_if_fail(conv != NULL);
7878 /* clear unseen flag if conversation is not hidden */
7879 if(!pidgin_conv_is_hidden(gtkconv)) {
7880 gtkconv_set_unseen(gtkconv, PIDGIN_UNSEEN_NONE);
7883 /* Update the menubar */
7885 gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(gtkconv->win->menu->logging),
7886 purple_conversation_is_logging(conv));
7888 generate_send_to_items(win);
7889 generate_e2ee_controls(win);
7890 regenerate_options_items(win);
7891 regenerate_plugins_items(win);
7893 pidgin_conv_switch_active_conversation(conv);
7895 sound_method = purple_prefs_get_string(PIDGIN_PREFS_ROOT "/sound/method");
7896 if (!purple_strequal(sound_method, "none"))
7897 gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(win->menu->sounds),
7898 gtkconv->make_sound);
7900 gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(win->menu->show_formatting_toolbar),
7901 purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/show_formatting_toolbar"));
7904 * We pause icons when they are not visible. If this icon should
7905 * be animated then start it back up again.
7907 if (PURPLE_IS_IM_CONVERSATION(conv) &&
7908 (gtkconv->u.im->animate))
7909 start_anim(NULL, gtkconv);
7911 purple_signal_emit(pidgin_conversations_get_handle(), "conversation-switched", conv);
7914 /**************************************************************************
7915 * GTK+ window ops
7916 **************************************************************************/
7918 GList *
7919 pidgin_conv_windows_get_list()
7921 return window_list;
7924 static GList*
7925 make_status_icon_list(const char *stock, GtkWidget *w)
7927 GList *l = NULL;
7928 l = g_list_append(l,
7929 gtk_widget_render_icon(w, stock,
7930 gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_EXTRA_SMALL), "GtkWindow"));
7931 l = g_list_append(l,
7932 gtk_widget_render_icon(w, stock,
7933 gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_SMALL), "GtkWindow"));
7934 l = g_list_append(l,
7935 gtk_widget_render_icon(w, stock,
7936 gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_MEDIUM), "GtkWindow"));
7937 l = g_list_append(l,
7938 gtk_widget_render_icon(w, stock,
7939 gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_LARGE), "GtkWindow"));
7940 return l;
7943 static void
7944 create_icon_lists(GtkWidget *w)
7946 available_list = make_status_icon_list(PIDGIN_STOCK_STATUS_AVAILABLE, w);
7947 busy_list = make_status_icon_list(PIDGIN_STOCK_STATUS_BUSY, w);
7948 xa_list = make_status_icon_list(PIDGIN_STOCK_STATUS_XA, w);
7949 offline_list = make_status_icon_list(PIDGIN_STOCK_STATUS_OFFLINE, w);
7950 away_list = make_status_icon_list(PIDGIN_STOCK_STATUS_AWAY, w);
7951 protocol_lists = g_hash_table_new(g_str_hash, g_str_equal);
7954 static void
7955 plugin_changed_cb(PurplePlugin *p, gpointer data)
7957 regenerate_plugins_items(data);
7960 static gboolean gtk_conv_configure_cb(GtkWidget *w, GdkEventConfigure *event, gpointer data) {
7961 int x, y;
7963 if (gtk_widget_get_visible(w))
7964 gtk_window_get_position(GTK_WINDOW(w), &x, &y);
7965 else
7966 return FALSE; /* carry on normally */
7968 /* Workaround for GTK+ bug # 169811 - "configure_event" is fired
7969 * when the window is being maximized */
7970 if (gdk_window_get_state(gtk_widget_get_window(w)) & GDK_WINDOW_STATE_MAXIMIZED)
7971 return FALSE;
7973 /* don't save off-screen positioning */
7974 if (x + event->width < 0 ||
7975 y + event->height < 0 ||
7976 x > gdk_screen_width() ||
7977 y > gdk_screen_height())
7978 return FALSE; /* carry on normally */
7980 /* store the position */
7981 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/x", x);
7982 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/y", y);
7983 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/width", event->width);
7984 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/height", event->height);
7986 /* continue to handle event normally */
7987 return FALSE;
7991 static void
7992 pidgin_conv_set_position_size(PidginConvWindow *win, int conv_x, int conv_y,
7993 int conv_width, int conv_height)
7995 /* if the window exists, is hidden, we're saving positions, and the
7996 * position is sane... */
7997 if (win && win->window &&
7998 !gtk_widget_get_visible(win->window) && conv_width != 0) {
8000 #ifdef _WIN32 /* only override window manager placement on Windows */
8001 /* ...check position is on screen... */
8002 if (conv_x >= gdk_screen_width())
8003 conv_x = gdk_screen_width() - 100;
8004 else if (conv_x + conv_width < 0)
8005 conv_x = 100;
8007 if (conv_y >= gdk_screen_height())
8008 conv_y = gdk_screen_height() - 100;
8009 else if (conv_y + conv_height < 0)
8010 conv_y = 100;
8012 /* ...and move it back. */
8013 gtk_window_move(GTK_WINDOW(win->window), conv_x, conv_y);
8014 #endif
8015 gtk_window_resize(GTK_WINDOW(win->window), conv_width, conv_height);
8019 static void
8020 pidgin_conv_restore_position(PidginConvWindow *win) {
8021 pidgin_conv_set_position_size(win,
8022 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/x"),
8023 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/y"),
8024 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/width"),
8025 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/height"));
8028 PidginConvWindow *
8029 pidgin_conv_window_new()
8031 PidginConvWindow *win;
8032 GtkPositionType pos;
8033 GtkWidget *testidea;
8034 GtkWidget *menubar;
8035 GtkWidget *menu;
8036 GtkWidget *item;
8037 GdkModifierType state;
8039 win = g_malloc0(sizeof(PidginConvWindow));
8040 win->menu = g_malloc0(sizeof(PidginConvWindowMenu));
8042 window_list = g_list_append(window_list, win);
8044 /* Create the window. */
8045 win->window = pidgin_create_window(NULL, 0, "conversation", TRUE);
8046 /*_pidgin_widget_set_accessible_name(win->window, "Conversations");*/
8047 if (!gtk_get_current_event_state(&state))
8048 gtk_window_set_focus_on_map(GTK_WINDOW(win->window), FALSE);
8050 /* Etan: I really think this entire function call should happen only
8051 * when we are on Windows but I was informed that back before we used
8052 * to save the window position we stored the window size, so I'm
8053 * leaving it for now. */
8054 #if TRUE || defined(_WIN32)
8055 pidgin_conv_restore_position(win);
8056 #endif
8058 if (available_list == NULL) {
8059 create_icon_lists(win->window);
8062 g_signal_connect(G_OBJECT(win->window), "delete_event",
8063 G_CALLBACK(close_win_cb), win);
8064 g_signal_connect(G_OBJECT(win->window), "focus_in_event",
8065 G_CALLBACK(focus_win_cb), win);
8067 /* Intercept keystrokes from the menu items */
8068 g_signal_connect(G_OBJECT(win->window), "key_press_event",
8069 G_CALLBACK(window_keypress_cb), win);
8072 /* Create the notebook. */
8073 win->notebook = gtk_notebook_new();
8075 pos = purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side");
8077 gtk_notebook_set_tab_pos(GTK_NOTEBOOK(win->notebook), pos);
8078 gtk_notebook_set_scrollable(GTK_NOTEBOOK(win->notebook), TRUE);
8079 gtk_notebook_set_show_tabs(GTK_NOTEBOOK(win->notebook), FALSE);
8080 gtk_notebook_set_show_border(GTK_NOTEBOOK(win->notebook), TRUE);
8082 menu = win->notebook_menu = gtk_menu_new();
8084 pidgin_separator(GTK_WIDGET(menu));
8086 item = gtk_menu_item_new_with_label(_("Close other tabs"));
8087 gtk_widget_show(item);
8088 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
8089 g_signal_connect(G_OBJECT(item), "activate",
8090 G_CALLBACK(close_others_cb), win);
8092 item = gtk_menu_item_new_with_label(_("Close all tabs"));
8093 gtk_widget_show(item);
8094 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
8095 g_signal_connect(G_OBJECT(item), "activate",
8096 G_CALLBACK(close_window), win);
8098 pidgin_separator(menu);
8100 item = gtk_menu_item_new_with_label(_("Detach this tab"));
8101 gtk_widget_show(item);
8102 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
8103 g_signal_connect(G_OBJECT(item), "activate",
8104 G_CALLBACK(detach_tab_cb), win);
8106 item = gtk_menu_item_new_with_label(_("Close this tab"));
8107 gtk_widget_show(item);
8108 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
8109 g_signal_connect(G_OBJECT(item), "activate",
8110 G_CALLBACK(close_tab_cb), win);
8112 g_signal_connect(G_OBJECT(win->notebook), "page-added",
8113 G_CALLBACK(notebook_add_tab_to_menu_cb), win);
8114 g_signal_connect(G_OBJECT(win->notebook), "page-removed",
8115 G_CALLBACK(notebook_remove_tab_from_menu_cb), win);
8116 g_signal_connect(G_OBJECT(win->notebook), "page-reordered",
8117 G_CALLBACK(notebook_reorder_tab_in_menu_cb), win);
8119 g_signal_connect(G_OBJECT(win->notebook), "button-press-event",
8120 G_CALLBACK(notebook_right_click_menu_cb), win);
8122 gtk_widget_show(win->notebook);
8124 g_signal_connect(G_OBJECT(win->notebook), "switch_page",
8125 G_CALLBACK(before_switch_conv_cb), win);
8126 g_signal_connect_after(G_OBJECT(win->notebook), "switch_page",
8127 G_CALLBACK(switch_conv_cb), win);
8129 /* Setup the tab drag and drop signals. */
8130 gtk_widget_add_events(win->notebook,
8131 GDK_BUTTON1_MOTION_MASK | GDK_LEAVE_NOTIFY_MASK);
8132 g_signal_connect(G_OBJECT(win->notebook), "button_press_event",
8133 G_CALLBACK(notebook_press_cb), win);
8134 g_signal_connect(G_OBJECT(win->notebook), "button_release_event",
8135 G_CALLBACK(notebook_release_cb), win);
8137 testidea = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
8139 /* Setup the menubar. */
8140 menubar = setup_menubar(win);
8141 gtk_box_pack_start(GTK_BOX(testidea), menubar, FALSE, TRUE, 0);
8143 gtk_box_pack_start(GTK_BOX(testidea), win->notebook, TRUE, TRUE, 0);
8145 gtk_container_add(GTK_CONTAINER(win->window), testidea);
8147 gtk_widget_show(testidea);
8149 /* Update the plugin actions when plugins are (un)loaded */
8150 purple_signal_connect(purple_plugins_get_handle(), "plugin-load",
8151 win, PURPLE_CALLBACK(plugin_changed_cb), win);
8152 purple_signal_connect(purple_plugins_get_handle(), "plugin-unload",
8153 win, PURPLE_CALLBACK(plugin_changed_cb), win);
8156 #ifdef _WIN32
8157 g_signal_connect(G_OBJECT(win->window), "show",
8158 G_CALLBACK(winpidgin_ensure_onscreen), win->window);
8160 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/win32/minimize_new_convs")
8161 && !gtk_get_current_event_state(&state))
8162 gtk_window_iconify(GTK_WINDOW(win->window));
8163 #endif
8165 purple_signal_emit(pidgin_conversations_get_handle(),
8166 "conversation-window-created", win);
8168 /* Fix colours */
8169 pidgin_conversations_set_tab_colors();
8171 return win;
8174 void
8175 pidgin_conv_window_destroy(PidginConvWindow *win)
8177 if (win->gtkconvs) {
8178 GList *iter = win->gtkconvs;
8179 while (iter)
8181 PidginConversation *gtkconv = iter->data;
8182 iter = iter->next;
8183 close_conv_cb(NULL, gtkconv);
8185 return;
8188 purple_prefs_disconnect_by_handle(win);
8189 window_list = g_list_remove(window_list, win);
8191 gtk_widget_destroy(win->notebook_menu);
8192 gtk_widget_destroy(win->window);
8194 g_object_unref(G_OBJECT(win->menu->ui));
8196 purple_notify_close_with_handle(win);
8197 purple_signals_disconnect_by_handle(win);
8199 g_free(win->menu);
8200 g_free(win);
8203 void
8204 pidgin_conv_window_show(PidginConvWindow *win)
8206 gtk_widget_show(win->window);
8209 void
8210 pidgin_conv_window_hide(PidginConvWindow *win)
8212 gtk_widget_hide(win->window);
8215 void
8216 pidgin_conv_window_raise(PidginConvWindow *win)
8218 gdk_window_raise(GDK_WINDOW(gtk_widget_get_window(win->window)));
8221 void
8222 pidgin_conv_window_switch_gtkconv(PidginConvWindow *win, PidginConversation *gtkconv)
8224 gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook),
8225 gtk_notebook_page_num(GTK_NOTEBOOK(win->notebook),
8226 gtkconv->tab_cont));
8229 static gboolean
8230 gtkconv_tab_set_tip(GtkWidget *widget, GdkEventCrossing *event, PidginConversation *gtkconv)
8232 /* PANGO_VERSION_CHECK macro was introduced in 1.15. So we need this double check. */
8233 #ifndef PANGO_VERSION_CHECK
8234 #define pango_layout_is_ellipsized(l) TRUE
8235 #elif !PANGO_VERSION_CHECK(1,16,0)
8236 #define pango_layout_is_ellipsized(l) TRUE
8237 #endif
8238 PangoLayout *layout;
8240 layout = gtk_label_get_layout(GTK_LABEL(gtkconv->tab_label));
8241 if (pango_layout_is_ellipsized(layout))
8242 gtk_widget_set_tooltip_text(widget, gtk_label_get_text(GTK_LABEL(gtkconv->tab_label)));
8243 else
8244 gtk_widget_set_tooltip_text(widget, NULL);
8246 return FALSE;
8249 static void
8250 set_default_tab_colors(GtkWidget *widget)
8252 GString *str;
8253 GtkCssProvider *provider;
8254 GError *error = NULL;
8255 int iter;
8257 struct {
8258 const char *labelname;
8259 const char *color;
8260 } styles[] = {
8261 {"tab-label-typing", "#4e9a06"},
8262 {"tab-label-typed", "#c4a000"},
8263 {"tab-label-attention", "#006aff"},
8264 {"tab-label-unreadchat", "#cc0000"},
8265 {"tab-label-event", "#888a85"},
8266 {NULL, NULL}
8269 str = g_string_new(NULL);
8271 for (iter = 0; styles[iter].labelname; iter++) {
8272 g_string_append_printf(str,
8273 "#%s {\n"
8274 " color: %s;\n"
8275 "}\n",
8276 styles[iter].labelname,
8277 styles[iter].color);
8280 provider = gtk_css_provider_new();
8282 gtk_css_provider_load_from_data(provider, str->str, str->len, &error);
8284 gtk_style_context_add_provider(gtk_widget_get_style_context(widget),
8285 GTK_STYLE_PROVIDER(provider),
8286 GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
8288 if (error)
8289 g_error_free(error);
8290 g_string_free(str, TRUE);
8293 void
8294 pidgin_conv_window_add_gtkconv(PidginConvWindow *win, PidginConversation *gtkconv)
8296 PurpleConversation *conv = gtkconv->active_conv;
8297 PidginConversation *focus_gtkconv;
8298 GtkWidget *tab_cont = gtkconv->tab_cont;
8299 const gchar *tmp_lab;
8301 win->gtkconvs = g_list_append(win->gtkconvs, gtkconv);
8302 gtkconv->win = win;
8304 if (win->gtkconvs && win->gtkconvs->next && win->gtkconvs->next->next == NULL)
8305 pidgin_conv_tab_pack(win, ((PidginConversation*)win->gtkconvs->data));
8308 /* Close button. */
8309 gtkconv->close = pidgin_create_small_button(gtk_label_new("×"));
8310 gtk_widget_set_tooltip_text(gtkconv->close, _("Close conversation"));
8312 g_signal_connect(gtkconv->close, "clicked", G_CALLBACK (close_conv_cb), gtkconv);
8314 /* Status icon. */
8315 gtkconv->icon = gtk_image_new();
8316 gtkconv->menu_icon = gtk_image_new();
8317 g_object_set(G_OBJECT(gtkconv->icon),
8318 "icon-size", gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_MICROSCOPIC),
8319 NULL);
8320 g_object_set(G_OBJECT(gtkconv->menu_icon),
8321 "icon-size", gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_MICROSCOPIC),
8322 NULL);
8323 gtk_widget_show(gtkconv->icon);
8324 update_tab_icon(conv);
8326 /* Tab label. */
8327 gtkconv->tab_label = gtk_label_new(tmp_lab = purple_conversation_get_title(conv));
8328 set_default_tab_colors(gtkconv->tab_label);
8329 gtk_widget_set_name(gtkconv->tab_label, "tab-label");
8331 gtkconv->menu_tabby = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, PIDGIN_HIG_BOX_SPACE);
8332 gtkconv->menu_label = gtk_label_new(tmp_lab);
8333 gtk_box_pack_start(GTK_BOX(gtkconv->menu_tabby), gtkconv->menu_icon, FALSE, FALSE, 0);
8335 gtk_widget_show_all(gtkconv->menu_icon);
8337 gtk_box_pack_start(GTK_BOX(gtkconv->menu_tabby), gtkconv->menu_label, TRUE, TRUE, 0);
8338 gtk_widget_show(gtkconv->menu_label);
8339 gtk_label_set_xalign(GTK_LABEL(gtkconv->menu_label), 0);
8340 gtk_label_set_yalign(GTK_LABEL(gtkconv->menu_label), 0);
8342 gtk_widget_show(gtkconv->menu_tabby);
8344 if (PURPLE_IS_IM_CONVERSATION(conv))
8345 pidgin_conv_update_buddy_icon(PURPLE_IM_CONVERSATION(conv));
8347 /* Build and set conversations tab */
8348 pidgin_conv_tab_pack(win, gtkconv);
8350 gtk_notebook_set_menu_label(GTK_NOTEBOOK(win->notebook), tab_cont, gtkconv->menu_tabby);
8352 gtk_widget_show(tab_cont);
8354 if (pidgin_conv_window_get_gtkconv_count(win) == 1) {
8355 /* Er, bug in notebooks? Switch to the page manually. */
8356 gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook), 0);
8357 } else {
8358 gtk_notebook_set_show_tabs(GTK_NOTEBOOK(win->notebook), TRUE);
8361 focus_gtkconv = g_list_nth_data(pidgin_conv_window_get_gtkconvs(win),
8362 gtk_notebook_get_current_page(GTK_NOTEBOOK(win->notebook)));
8363 gtk_widget_grab_focus(focus_gtkconv->editor);
8365 if (pidgin_conv_window_get_gtkconv_count(win) == 1)
8366 update_send_to_selection(win);
8369 static void
8370 pidgin_conv_tab_pack(PidginConvWindow *win, PidginConversation *gtkconv)
8372 gboolean tabs_side = FALSE;
8373 gint angle = 0;
8374 GtkWidget *first, *third, *ebox, *parent;
8376 if (purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side") == GTK_POS_LEFT ||
8377 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side") == GTK_POS_RIGHT)
8378 tabs_side = TRUE;
8379 else if (purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side") == (GTK_POS_LEFT|8))
8380 angle = 90;
8381 else if (purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side") == (GTK_POS_RIGHT|8))
8382 angle = 270;
8384 if (!angle) {
8385 g_object_set(G_OBJECT(gtkconv->tab_label), "ellipsize", PANGO_ELLIPSIZE_END, NULL);
8386 gtk_label_set_width_chars(GTK_LABEL(gtkconv->tab_label), 4);
8387 } else {
8388 g_object_set(G_OBJECT(gtkconv->tab_label), "ellipsize", PANGO_ELLIPSIZE_NONE, NULL);
8389 gtk_label_set_width_chars(GTK_LABEL(gtkconv->tab_label), -1);
8392 if (tabs_side) {
8393 gtk_label_set_width_chars(
8394 GTK_LABEL(gtkconv->tab_label),
8395 MIN(g_utf8_strlen(gtk_label_get_text(GTK_LABEL(gtkconv->tab_label)), -1), 12)
8399 gtk_label_set_angle(GTK_LABEL(gtkconv->tab_label), angle);
8401 if (angle)
8402 gtkconv->tabby = gtk_box_new(GTK_ORIENTATION_VERTICAL, PIDGIN_HIG_BOX_SPACE);
8403 else
8404 gtkconv->tabby = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, PIDGIN_HIG_BOX_SPACE);
8405 gtk_widget_set_name(gtkconv->tabby, "tab-container");
8407 /* select the correct ordering for verticle tabs */
8408 if (angle == 90) {
8409 first = gtkconv->close;
8410 third = gtkconv->icon;
8411 } else {
8412 first = gtkconv->icon;
8413 third = gtkconv->close;
8416 ebox = gtk_event_box_new();
8417 gtk_event_box_set_visible_window(GTK_EVENT_BOX(ebox), FALSE);
8418 gtk_container_add(GTK_CONTAINER(ebox), gtkconv->tabby);
8419 g_signal_connect(G_OBJECT(ebox), "enter-notify-event",
8420 G_CALLBACK(gtkconv_tab_set_tip), gtkconv);
8422 parent = gtk_widget_get_parent(gtkconv->tab_label);
8423 if (parent != NULL) {
8424 /* reparent old widgets on preference changes */
8425 g_object_ref(first);
8426 g_object_ref(gtkconv->tab_label);
8427 g_object_ref(third);
8428 gtk_container_remove(GTK_CONTAINER(parent), first);
8429 gtk_container_remove(GTK_CONTAINER(parent), gtkconv->tab_label);
8430 gtk_container_remove(GTK_CONTAINER(parent), third);
8433 gtk_box_pack_start(GTK_BOX(gtkconv->tabby), first, FALSE, FALSE, 0);
8434 gtk_box_pack_start(GTK_BOX(gtkconv->tabby), gtkconv->tab_label, TRUE, TRUE, 0);
8435 gtk_box_pack_start(GTK_BOX(gtkconv->tabby), third, FALSE, FALSE, 0);
8437 if (parent == NULL) {
8438 /* Add this pane to the conversation's notebook. */
8439 gtk_notebook_append_page(GTK_NOTEBOOK(win->notebook), gtkconv->tab_cont, ebox);
8440 } else {
8441 /* reparent old widgets on preference changes */
8442 g_object_unref(first);
8443 g_object_unref(gtkconv->tab_label);
8444 g_object_unref(third);
8446 /* Reset the tabs label to the new version */
8447 gtk_notebook_set_tab_label(GTK_NOTEBOOK(win->notebook), gtkconv->tab_cont, ebox);
8450 gtk_container_child_set(GTK_CONTAINER(win->notebook), gtkconv->tab_cont,
8451 "tab-expand", !tabs_side && !angle,
8452 "tab-fill", TRUE, NULL);
8454 if (pidgin_conv_window_get_gtkconv_count(win) == 1)
8455 gtk_notebook_set_show_tabs(GTK_NOTEBOOK(win->notebook),
8456 purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/tabs") &&
8457 (!purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/im/show_buddy_icons") ||
8458 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side") != GTK_POS_TOP));
8460 /* show the widgets */
8461 /* gtk_widget_show(gtkconv->icon); */
8462 gtk_widget_show(gtkconv->tab_label);
8463 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/close_on_tabs"))
8464 gtk_widget_show(gtkconv->close);
8465 gtk_widget_show(gtkconv->tabby);
8466 gtk_widget_show(ebox);
8469 void
8470 pidgin_conv_window_remove_gtkconv(PidginConvWindow *win, PidginConversation *gtkconv)
8472 unsigned int index;
8474 index = gtk_notebook_page_num(GTK_NOTEBOOK(win->notebook), gtkconv->tab_cont);
8476 g_object_ref_sink(G_OBJECT(gtkconv->tab_cont));
8478 gtk_notebook_remove_page(GTK_NOTEBOOK(win->notebook), index);
8480 win->gtkconvs = g_list_remove(win->gtkconvs, gtkconv);
8482 g_signal_handlers_disconnect_matched(win->window, G_SIGNAL_MATCH_DATA,
8483 0, 0, NULL, NULL, gtkconv);
8485 if (win->gtkconvs && win->gtkconvs->next == NULL)
8486 pidgin_conv_tab_pack(win, win->gtkconvs->data);
8488 if (!win->gtkconvs && win != hidden_convwin)
8489 pidgin_conv_window_destroy(win);
8492 PidginConversation *
8493 pidgin_conv_window_get_gtkconv_at_index(const PidginConvWindow *win, int index)
8495 GtkWidget *tab_cont;
8497 if (index == -1)
8498 index = 0;
8499 tab_cont = gtk_notebook_get_nth_page(GTK_NOTEBOOK(win->notebook), index);
8500 return tab_cont ? g_object_get_data(G_OBJECT(tab_cont), "PidginConversation") : NULL;
8503 PidginConversation *
8504 pidgin_conv_window_get_active_gtkconv(const PidginConvWindow *win)
8506 int index;
8507 GtkWidget *tab_cont;
8509 index = gtk_notebook_get_current_page(GTK_NOTEBOOK(win->notebook));
8510 if (index == -1)
8511 index = 0;
8512 tab_cont = gtk_notebook_get_nth_page(GTK_NOTEBOOK(win->notebook), index);
8513 if (!tab_cont)
8514 return NULL;
8515 return g_object_get_data(G_OBJECT(tab_cont), "PidginConversation");
8519 PurpleConversation *
8520 pidgin_conv_window_get_active_conversation(const PidginConvWindow *win)
8522 PidginConversation *gtkconv;
8524 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
8525 return gtkconv ? gtkconv->active_conv : NULL;
8528 gboolean
8529 pidgin_conv_window_is_active_conversation(const PurpleConversation *conv)
8531 return conv == pidgin_conv_window_get_active_conversation(PIDGIN_CONVERSATION(conv)->win);
8534 gboolean
8535 pidgin_conv_window_has_focus(PidginConvWindow *win)
8537 gboolean has_focus = FALSE;
8539 g_object_get(G_OBJECT(win->window), "has-toplevel-focus", &has_focus, NULL);
8541 return has_focus;
8544 PidginConvWindow *
8545 pidgin_conv_window_get_at_event(GdkEvent *event)
8547 PidginConvWindow *win;
8548 GdkWindow *gdkwin;
8549 GList *l;
8550 int x, y;
8552 gdkwin = gdk_device_get_window_at_position(gdk_event_get_device(event),
8553 &x, &y);
8555 if (gdkwin)
8556 gdkwin = gdk_window_get_toplevel(gdkwin);
8558 for (l = pidgin_conv_windows_get_list(); l != NULL; l = l->next) {
8559 win = l->data;
8561 if (gdkwin == gtk_widget_get_window(win->window))
8562 return win;
8565 return NULL;
8568 GList *
8569 pidgin_conv_window_get_gtkconvs(PidginConvWindow *win)
8571 return win->gtkconvs;
8574 guint
8575 pidgin_conv_window_get_gtkconv_count(PidginConvWindow *win)
8577 return g_list_length(win->gtkconvs);
8580 PidginConvWindow *
8581 pidgin_conv_window_first_im(void)
8583 GList *wins, *convs;
8584 PidginConvWindow *win;
8585 PidginConversation *conv;
8587 for (wins = pidgin_conv_windows_get_list(); wins != NULL; wins = wins->next) {
8588 win = wins->data;
8590 for (convs = win->gtkconvs;
8591 convs != NULL;
8592 convs = convs->next) {
8594 conv = convs->data;
8596 if (PURPLE_IS_IM_CONVERSATION(conv->active_conv))
8597 return win;
8601 return NULL;
8604 PidginConvWindow *
8605 pidgin_conv_window_last_im(void)
8607 GList *wins, *convs;
8608 PidginConvWindow *win;
8609 PidginConversation *conv;
8611 for (wins = g_list_last(pidgin_conv_windows_get_list());
8612 wins != NULL;
8613 wins = wins->prev) {
8615 win = wins->data;
8617 for (convs = win->gtkconvs;
8618 convs != NULL;
8619 convs = convs->next) {
8621 conv = convs->data;
8623 if (PURPLE_IS_IM_CONVERSATION(conv->active_conv))
8624 return win;
8628 return NULL;
8631 PidginConvWindow *
8632 pidgin_conv_window_first_chat(void)
8634 GList *wins, *convs;
8635 PidginConvWindow *win;
8636 PidginConversation *conv;
8638 for (wins = pidgin_conv_windows_get_list(); wins != NULL; wins = wins->next) {
8639 win = wins->data;
8641 for (convs = win->gtkconvs;
8642 convs != NULL;
8643 convs = convs->next) {
8645 conv = convs->data;
8647 if (PURPLE_IS_CHAT_CONVERSATION(conv->active_conv))
8648 return win;
8652 return NULL;
8655 PidginConvWindow *
8656 pidgin_conv_window_last_chat(void)
8658 GList *wins, *convs;
8659 PidginConvWindow *win;
8660 PidginConversation *conv;
8662 for (wins = g_list_last(pidgin_conv_windows_get_list());
8663 wins != NULL;
8664 wins = wins->prev) {
8666 win = wins->data;
8668 for (convs = win->gtkconvs;
8669 convs != NULL;
8670 convs = convs->next) {
8672 conv = convs->data;
8674 if (PURPLE_IS_CHAT_CONVERSATION(conv->active_conv))
8675 return win;
8679 return NULL;
8683 /**************************************************************************
8684 * Conversation placement functions
8685 **************************************************************************/
8686 typedef struct
8688 char *id;
8689 char *name;
8690 PidginConvPlacementFunc fnc;
8692 } ConvPlacementData;
8694 static GList *conv_placement_fncs = NULL;
8695 static PidginConvPlacementFunc place_conv = NULL;
8697 /* This one places conversations in the last made window. */
8698 static void
8699 conv_placement_last_created_win(PidginConversation *conv)
8701 PidginConvWindow *win;
8703 GList *l = g_list_last(pidgin_conv_windows_get_list());
8704 win = l ? l->data : NULL;;
8706 if (win == NULL) {
8707 win = pidgin_conv_window_new();
8709 g_signal_connect(G_OBJECT(win->window), "configure_event",
8710 G_CALLBACK(gtk_conv_configure_cb), NULL);
8712 pidgin_conv_window_add_gtkconv(win, conv);
8713 pidgin_conv_window_show(win);
8714 } else {
8715 pidgin_conv_window_add_gtkconv(win, conv);
8719 /* This one places conversations in the last made window of the same type. */
8720 static gboolean
8721 conv_placement_last_created_win_type_configured_cb(GtkWidget *w,
8722 GdkEventConfigure *event, PidginConversation *conv)
8724 int x, y;
8725 GList *all;
8727 if (gtk_widget_get_visible(w))
8728 gtk_window_get_position(GTK_WINDOW(w), &x, &y);
8729 else
8730 return FALSE; /* carry on normally */
8732 /* Workaround for GTK+ bug # 169811 - "configure_event" is fired
8733 * when the window is being maximized */
8734 if (gdk_window_get_state(gtk_widget_get_window(w)) & GDK_WINDOW_STATE_MAXIMIZED)
8735 return FALSE;
8737 /* don't save off-screen positioning */
8738 if (x + event->width < 0 ||
8739 y + event->height < 0 ||
8740 x > gdk_screen_width() ||
8741 y > gdk_screen_height())
8742 return FALSE; /* carry on normally */
8744 for (all = conv->convs; all != NULL; all = all->next) {
8745 if (PURPLE_IS_IM_CONVERSATION(conv->active_conv) != PURPLE_IS_IM_CONVERSATION(all->data)) {
8746 /* this window has different types of conversation, don't save */
8747 return FALSE;
8751 if (PURPLE_IS_IM_CONVERSATION(conv->active_conv)) {
8752 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/x", x);
8753 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/y", y);
8754 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/width", event->width);
8755 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/height", event->height);
8756 } else if (PURPLE_IS_CHAT_CONVERSATION(conv->active_conv)) {
8757 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/chat/x", x);
8758 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/chat/y", y);
8759 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/chat/width", event->width);
8760 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/chat/height", event->height);
8763 return FALSE;
8766 static void
8767 conv_placement_last_created_win_type(PidginConversation *conv)
8769 PidginConvWindow *win;
8771 if (PURPLE_IS_IM_CONVERSATION(conv->active_conv))
8772 win = pidgin_conv_window_last_im();
8773 else
8774 win = pidgin_conv_window_last_chat();
8776 if (win == NULL) {
8777 win = pidgin_conv_window_new();
8779 if (PURPLE_IS_IM_CONVERSATION(conv->active_conv) ||
8780 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/chat/width") == 0) {
8781 pidgin_conv_set_position_size(win,
8782 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/x"),
8783 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/y"),
8784 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/width"),
8785 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/height"));
8786 } else if (PURPLE_IS_CHAT_CONVERSATION(conv->active_conv)) {
8787 pidgin_conv_set_position_size(win,
8788 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/chat/x"),
8789 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/chat/y"),
8790 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/chat/width"),
8791 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/chat/height"));
8794 pidgin_conv_window_add_gtkconv(win, conv);
8795 pidgin_conv_window_show(win);
8797 g_signal_connect(G_OBJECT(win->window), "configure_event",
8798 G_CALLBACK(conv_placement_last_created_win_type_configured_cb), conv);
8799 } else
8800 pidgin_conv_window_add_gtkconv(win, conv);
8803 /* This one places each conversation in its own window. */
8804 static void
8805 conv_placement_new_window(PidginConversation *conv)
8807 PidginConvWindow *win;
8809 win = pidgin_conv_window_new();
8811 g_signal_connect(G_OBJECT(win->window), "configure_event",
8812 G_CALLBACK(gtk_conv_configure_cb), NULL);
8814 pidgin_conv_window_add_gtkconv(win, conv);
8816 pidgin_conv_window_show(win);
8819 static PurpleGroup *
8820 conv_get_group(PidginConversation *conv)
8822 PurpleGroup *group = NULL;
8824 if (PURPLE_IS_IM_CONVERSATION(conv->active_conv)) {
8825 PurpleBuddy *buddy;
8827 buddy = purple_blist_find_buddy(purple_conversation_get_account(conv->active_conv),
8828 purple_conversation_get_name(conv->active_conv));
8830 if (buddy != NULL)
8831 group = purple_buddy_get_group(buddy);
8833 } else if (PURPLE_IS_CHAT_CONVERSATION(conv->active_conv)) {
8834 PurpleChat *chat;
8836 chat = purple_blist_find_chat(purple_conversation_get_account(conv->active_conv),
8837 purple_conversation_get_name(conv->active_conv));
8839 if (chat != NULL)
8840 group = purple_chat_get_group(chat);
8843 return group;
8847 * This groups things by, well, group. Buddies from groups will always be
8848 * grouped together, and a buddy from a group not belonging to any currently
8849 * open windows will get a new window.
8851 static void
8852 conv_placement_by_group(PidginConversation *conv)
8854 PurpleGroup *group = NULL;
8855 GList *wl, *cl;
8857 group = conv_get_group(conv);
8859 /* Go through the list of IMs and find one with this group. */
8860 for (wl = pidgin_conv_windows_get_list(); wl != NULL; wl = wl->next) {
8861 PidginConvWindow *win2;
8862 PidginConversation *conv2;
8863 PurpleGroup *group2 = NULL;
8865 win2 = wl->data;
8867 for (cl = win2->gtkconvs;
8868 cl != NULL;
8869 cl = cl->next) {
8870 conv2 = cl->data;
8872 group2 = conv_get_group(conv2);
8874 if (group == group2) {
8875 pidgin_conv_window_add_gtkconv(win2, conv);
8877 return;
8882 /* Make a new window. */
8883 conv_placement_new_window(conv);
8886 /* This groups things by account. Otherwise, the same semantics as above */
8887 static void
8888 conv_placement_by_account(PidginConversation *conv)
8890 GList *wins, *convs;
8891 PurpleAccount *account;
8893 account = purple_conversation_get_account(conv->active_conv);
8895 /* Go through the list of IMs and find one with this group. */
8896 for (wins = pidgin_conv_windows_get_list(); wins != NULL; wins = wins->next) {
8897 PidginConvWindow *win2;
8898 PidginConversation *conv2;
8900 win2 = wins->data;
8902 for (convs = win2->gtkconvs;
8903 convs != NULL;
8904 convs = convs->next) {
8905 conv2 = convs->data;
8907 if (account == purple_conversation_get_account(conv2->active_conv)) {
8908 pidgin_conv_window_add_gtkconv(win2, conv);
8909 return;
8914 /* Make a new window. */
8915 conv_placement_new_window(conv);
8918 static ConvPlacementData *
8919 get_conv_placement_data(const char *id)
8921 ConvPlacementData *data = NULL;
8922 GList *n;
8924 for (n = conv_placement_fncs; n; n = n->next) {
8925 data = n->data;
8926 if (purple_strequal(data->id, id))
8927 return data;
8930 return NULL;
8933 static void
8934 add_conv_placement_fnc(const char *id, const char *name,
8935 PidginConvPlacementFunc fnc)
8937 ConvPlacementData *data;
8939 data = g_new(ConvPlacementData, 1);
8941 data->id = g_strdup(id);
8942 data->name = g_strdup(name);
8943 data->fnc = fnc;
8945 conv_placement_fncs = g_list_append(conv_placement_fncs, data);
8948 static void
8949 ensure_default_funcs(void)
8951 if (conv_placement_fncs == NULL) {
8952 add_conv_placement_fnc("last", _("Last created window"),
8953 conv_placement_last_created_win);
8954 add_conv_placement_fnc("im_chat", _("Separate IM and Chat windows"),
8955 conv_placement_last_created_win_type);
8956 add_conv_placement_fnc("new", _("New window"),
8957 conv_placement_new_window);
8958 add_conv_placement_fnc("group", _("By group"),
8959 conv_placement_by_group);
8960 add_conv_placement_fnc("account", _("By account"),
8961 conv_placement_by_account);
8965 GList *
8966 pidgin_conv_placement_get_options(void)
8968 GList *n, *list = NULL;
8969 ConvPlacementData *data;
8971 ensure_default_funcs();
8973 for (n = conv_placement_fncs; n; n = n->next) {
8974 data = n->data;
8975 list = g_list_append(list, data->name);
8976 list = g_list_append(list, data->id);
8979 return list;
8983 void
8984 pidgin_conv_placement_add_fnc(const char *id, const char *name,
8985 PidginConvPlacementFunc fnc)
8987 g_return_if_fail(id != NULL);
8988 g_return_if_fail(name != NULL);
8989 g_return_if_fail(fnc != NULL);
8991 ensure_default_funcs();
8993 add_conv_placement_fnc(id, name, fnc);
8996 void
8997 pidgin_conv_placement_remove_fnc(const char *id)
8999 ConvPlacementData *data = get_conv_placement_data(id);
9001 if (data == NULL)
9002 return;
9004 conv_placement_fncs = g_list_remove(conv_placement_fncs, data);
9006 g_free(data->id);
9007 g_free(data->name);
9008 g_free(data);
9011 const char *
9012 pidgin_conv_placement_get_name(const char *id)
9014 ConvPlacementData *data;
9016 ensure_default_funcs();
9018 data = get_conv_placement_data(id);
9020 if (data == NULL)
9021 return NULL;
9023 return data->name;
9026 PidginConvPlacementFunc
9027 pidgin_conv_placement_get_fnc(const char *id)
9029 ConvPlacementData *data;
9031 ensure_default_funcs();
9033 data = get_conv_placement_data(id);
9035 if (data == NULL)
9036 return NULL;
9038 return data->fnc;
9041 void
9042 pidgin_conv_placement_set_current_func(PidginConvPlacementFunc func)
9044 g_return_if_fail(func != NULL);
9046 /* If tabs are enabled, set the function, otherwise, NULL it out. */
9047 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/tabs"))
9048 place_conv = func;
9049 else
9050 place_conv = NULL;
9053 PidginConvPlacementFunc
9054 pidgin_conv_placement_get_current_func(void)
9056 return place_conv;
9059 void
9060 pidgin_conv_placement_place(PidginConversation *gtkconv)
9062 if (place_conv)
9063 place_conv(gtkconv);
9064 else
9065 conv_placement_new_window(gtkconv);
9068 gboolean
9069 pidgin_conv_is_hidden(PidginConversation *gtkconv)
9071 g_return_val_if_fail(gtkconv != NULL, FALSE);
9073 return (gtkconv->win == hidden_convwin);
9077 gdouble luminance(GdkRGBA color)
9079 gdouble r, g, b;
9080 gdouble rr, gg, bb;
9081 gdouble cutoff = 0.03928, scale = 12.92;
9082 gdouble a = 0.055, d = 1.055, p = 2.2;
9084 rr = color.red;
9085 gg = color.green;
9086 bb = color.blue;
9088 r = (rr > cutoff) ? pow((rr+a)/d, p) : rr/scale;
9089 g = (gg > cutoff) ? pow((gg+a)/d, p) : gg/scale;
9090 b = (bb > cutoff) ? pow((bb+a)/d, p) : bb/scale;
9092 return (r*0.2126 + g*0.7152 + b*0.0722);
9095 /* Algorithm from https://www.w3.org/TR/2008/REC-WCAG20-20081211/relative-luminance.xml */
9096 static gboolean
9097 color_is_visible(GdkRGBA foreground, GdkRGBA background, gdouble min_contrast_ratio)
9099 gdouble lfg, lbg, lmin, lmax;
9100 gdouble luminosity_ratio;
9101 gdouble nr, dr;
9103 lfg = luminance(foreground);
9104 lbg = luminance(background);
9106 if (lfg > lbg)
9107 lmax = lfg, lmin = lbg;
9108 else
9109 lmax = lbg, lmin = lfg;
9111 nr = lmax + 0.05, dr = lmin - 0.05;
9112 if (dr < 0.005 && dr > -0.005)
9113 dr += 0.01;
9115 luminosity_ratio = nr/dr;
9116 if ( luminosity_ratio < 0)
9117 luminosity_ratio *= -1.0;
9118 return (luminosity_ratio > min_contrast_ratio);
9122 static GArray*
9123 generate_nick_colors(guint numcolors, GdkRGBA background)
9125 guint i = 0, j = 0;
9126 GArray *colors = g_array_new(FALSE, FALSE, sizeof(GdkRGBA));
9127 GdkRGBA nick_highlight;
9128 GdkRGBA send_color;
9129 time_t breakout_time;
9131 gdk_rgba_parse(&nick_highlight, DEFAULT_HIGHLIGHT_COLOR);
9132 gdk_rgba_parse(&send_color, DEFAULT_SEND_COLOR);
9134 pidgin_style_adjust_contrast(NULL, &nick_highlight);
9135 pidgin_style_adjust_contrast(NULL, &send_color);
9137 srand(background.red * 65535 + background.green * 65535 + background.blue * 65535 + 1);
9139 breakout_time = time(NULL) + 3;
9141 /* first we look through the list of "good" colors: colors that differ from every other color in the
9142 * list. only some of them will differ from the background color though. lets see if we can find
9143 * numcolors of them that do
9145 while (i < numcolors && j < PIDGIN_NUM_NICK_SEED_COLORS && time(NULL) < breakout_time)
9147 GdkRGBA color = nick_seed_colors[j];
9149 if (color_is_visible(color, background, MIN_LUMINANCE_CONTRAST_RATIO) &&
9150 color_is_visible(color, nick_highlight, MIN_LUMINANCE_CONTRAST_RATIO) &&
9151 color_is_visible(color, send_color, MIN_LUMINANCE_CONTRAST_RATIO))
9153 g_array_append_val(colors, color);
9154 i++;
9156 j++;
9159 /* we might not have found numcolors in the last loop. if we did, we'll never enter this one.
9160 * if we did not, lets just find some colors that don't conflict with the background. its
9161 * expensive to find colors that not only don't conflict with the background, but also do not
9162 * conflict with each other.
9164 while(i < numcolors && time(NULL) < breakout_time)
9166 GdkRGBA color = {g_random_double_range(0, 1), g_random_double_range(0, 1), g_random_double_range(0, 1), 1};
9168 if (color_is_visible(color, background, MIN_LUMINANCE_CONTRAST_RATIO) &&
9169 color_is_visible(color, nick_highlight, MIN_LUMINANCE_CONTRAST_RATIO) &&
9170 color_is_visible(color, send_color, MIN_LUMINANCE_CONTRAST_RATIO))
9172 g_array_append_val(colors, color);
9173 i++;
9177 if (i < numcolors) {
9178 purple_debug_warning("gtkconv", "Unable to generate enough random colors before timeout. %u colors found.\n", i);
9181 if( i == 0 ) {
9182 /* To remove errors caused by an empty array. */
9183 GdkRGBA color = {0.5, 0.5, 0.5, 1.0};
9184 g_array_append_val(colors, color);
9187 return colors;
9190 /**************************************************************************
9191 * PidginConvWindow GBoxed code
9192 **************************************************************************/
9193 static PidginConvWindow *
9194 pidgin_conv_window_ref(PidginConvWindow *win)
9196 g_return_val_if_fail(win != NULL, NULL);
9198 win->box_count++;
9200 return win;
9203 static void
9204 pidgin_conv_window_unref(PidginConvWindow *win)
9206 g_return_if_fail(win != NULL);
9207 g_return_if_fail(win->box_count >= 0);
9209 if (!win->box_count--)
9210 pidgin_conv_window_destroy(win);
9213 GType
9214 pidgin_conv_window_get_type(void)
9216 static GType type = 0;
9218 if (type == 0) {
9219 type = g_boxed_type_register_static("PidginConvWindow",
9220 (GBoxedCopyFunc)pidgin_conv_window_ref,
9221 (GBoxedFreeFunc)pidgin_conv_window_unref);
9224 return type;