Silence various discarded const warnings.
[pidgin-git.git] / pidgin / gtkconv.c
blobe3a8e0d11922829b0e6dc8495cfbf2182dde3b2b
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 PurplePluginInfo *info = purple_plugin_get_info(PURPLE_PLUGIN(plugins->data));
328 str = g_string_append(str, purple_plugin_info_get_name(info));
330 if (plugins->next)
331 str = g_string_append(str, ", ");
333 } else {
334 str = g_string_append(str, "(none)");
337 tmp = g_string_free(str, FALSE);
338 } else if (!g_ascii_strcasecmp(args[0], "unsafe")) {
339 if (purple_debug_is_unsafe()) {
340 purple_debug_set_unsafe(FALSE);
341 purple_conversation_write_system_message(conv,
342 _("Unsafe debugging is now disabled."),
343 PURPLE_MESSAGE_NO_LOG);
344 } else {
345 purple_debug_set_unsafe(TRUE);
346 purple_conversation_write_system_message(conv,
347 _("Unsafe debugging is now enabled."),
348 PURPLE_MESSAGE_NO_LOG);
351 return PURPLE_CMD_RET_OK;
352 } else if (!g_ascii_strcasecmp(args[0], "verbose")) {
353 if (purple_debug_is_verbose()) {
354 purple_debug_set_verbose(FALSE);
355 purple_conversation_write_system_message(conv,
356 _("Verbose debugging is now disabled."),
357 PURPLE_MESSAGE_NO_LOG);
358 } else {
359 purple_debug_set_verbose(TRUE);
360 purple_conversation_write_system_message(conv,
361 _("Verbose debugging is now enabled."),
362 PURPLE_MESSAGE_NO_LOG);
365 return PURPLE_CMD_RET_OK;
366 } else {
367 purple_conversation_write_system_message(conv,
368 _("Supported debug options are: plugins, version, unsafe, verbose"),
369 PURPLE_MESSAGE_NO_LOG);
370 return PURPLE_CMD_RET_OK;
373 markup = g_markup_escape_text(tmp, -1);
374 purple_conversation_send(conv, markup);
376 g_free(tmp);
377 g_free(markup);
378 return PURPLE_CMD_RET_OK;
381 static void clear_conversation_scrollback_cb(PurpleConversation *conv,
382 void *data)
384 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
386 if (PIDGIN_CONVERSATION(conv)) {
387 gtkconv->last_flags = 0;
391 static PurpleCmdRet
392 clear_command_cb(PurpleConversation *conv,
393 const char *cmd, char **args, char **error, void *data)
395 purple_conversation_clear_message_history(conv);
396 return PURPLE_CMD_RET_OK;
399 static PurpleCmdRet
400 clearall_command_cb(PurpleConversation *conv,
401 const char *cmd, char **args, char **error, void *data)
403 GList *l;
404 for (l = purple_conversations_get_all(); l != NULL; l = l->next)
405 purple_conversation_clear_message_history(PURPLE_CONVERSATION(l->data));
407 return PURPLE_CMD_RET_OK;
410 static PurpleCmdRet
411 help_command_cb(PurpleConversation *conv,
412 const char *cmd, char **args, char **error, void *data)
414 GList *l, *text;
415 GString *s;
417 if (args[0] != NULL) {
418 s = g_string_new("");
419 text = purple_cmd_help(conv, args[0]);
421 if (text) {
422 for (l = text; l; l = l->next)
423 if (l->next)
424 g_string_append_printf(s, "%s\n", (char *)l->data);
425 else
426 g_string_append_printf(s, "%s", (char *)l->data);
427 } else {
428 g_string_append(s, _("No such command (in this context)."));
430 } else {
431 s = g_string_new(_("Use \"/help &lt;command&gt;\" for help with a "
432 "specific command.<br/>The following commands are available "
433 "in this context:<br/>"));
435 text = purple_cmd_list(conv);
436 for (l = text; l; l = l->next)
437 if (l->next)
438 g_string_append_printf(s, "%s, ", (char *)l->data);
439 else
440 g_string_append_printf(s, "%s.", (char *)l->data);
441 g_list_free(text);
444 purple_conversation_write_system_message(conv, s->str, PURPLE_MESSAGE_NO_LOG);
445 g_string_free(s, TRUE);
447 return PURPLE_CMD_RET_OK;
450 static void
451 send_history_add(PidginConversation *gtkconv, const char *message)
453 GList *first;
455 first = g_list_first(gtkconv->send_history);
456 g_free(first->data);
457 first->data = g_strdup(message);
458 gtkconv->send_history = g_list_prepend(first, NULL);
461 static gboolean
462 check_for_and_do_command(PurpleConversation *conv)
464 PidginConversation *gtkconv;
465 GtkWidget *view = NULL;
466 GtkTextBuffer *buffer = NULL;
467 gchar *cmd;
468 const gchar *prefix;
469 gboolean retval = FALSE;
471 gtkconv = PIDGIN_CONVERSATION(conv);
472 prefix = pidgin_get_cmd_prefix();
474 view = talkatu_editor_get_view(TALKATU_EDITOR(gtkconv->editor));
475 buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(view));
477 cmd = talkatu_buffer_get_plain_text(TALKATU_BUFFER(buffer));
479 if (cmd && purple_str_has_prefix(cmd, prefix)) {
480 PurpleCmdStatus status;
481 char *error, *cmdline, *markup, *send_history;
483 send_history = talkatu_markup_get_html(buffer, NULL);
484 send_history_add(gtkconv, send_history);
485 g_free(send_history);
487 cmdline = cmd + strlen(prefix);
489 if (purple_strequal(cmdline, "xyzzy")) {
490 purple_conversation_write_system_message(conv,
491 "Nothing happens", PURPLE_MESSAGE_NO_LOG);
492 g_free(cmd);
493 return TRUE;
496 /* Docs are unclear on whether or not prefix should be removed from
497 * the markup so, ignoring for now. Notably if the markup is
498 * `<b>/foo arg1</b>` we now have to move the bold tag around?
499 * - gk 20190709 */
500 markup = talkatu_markup_get_html(buffer, NULL);
501 status = purple_cmd_do_command(conv, cmdline, markup, &error);
502 g_free(markup);
504 switch (status) {
505 case PURPLE_CMD_STATUS_OK:
506 retval = TRUE;
507 break;
508 case PURPLE_CMD_STATUS_NOT_FOUND:
510 PurpleProtocol *protocol = NULL;
511 PurpleConnection *gc;
513 if ((gc = purple_conversation_get_connection(conv)))
514 protocol = purple_connection_get_protocol(gc);
516 if ((protocol != NULL) && (purple_protocol_get_options(protocol) & OPT_PROTO_SLASH_COMMANDS_NATIVE)) {
517 char *spaceslash;
519 /* If the first word in the entered text has a '/' in it, then the user
520 * probably didn't mean it as a command. So send the text as message. */
521 spaceslash = cmdline;
522 while (*spaceslash && *spaceslash != ' ' && *spaceslash != '/')
523 spaceslash++;
525 if (*spaceslash != '/') {
526 purple_conversation_write_system_message(conv,
527 _("Unknown command."), PURPLE_MESSAGE_NO_LOG);
528 retval = TRUE;
531 break;
533 case PURPLE_CMD_STATUS_WRONG_ARGS:
534 purple_conversation_write_system_message(conv,
535 _("Syntax Error: You typed the wrong "
536 "number of arguments to that command."),
537 PURPLE_MESSAGE_NO_LOG);
538 retval = TRUE;
539 break;
540 case PURPLE_CMD_STATUS_FAILED:
541 purple_conversation_write_system_message(conv,
542 error ? error : _("Your command failed for an unknown reason."),
543 PURPLE_MESSAGE_NO_LOG);
544 g_free(error);
545 retval = TRUE;
546 break;
547 case PURPLE_CMD_STATUS_WRONG_TYPE:
548 if(PURPLE_IS_IM_CONVERSATION(conv))
549 purple_conversation_write_system_message(conv,
550 _("That command only works in chats, not IMs."),
551 PURPLE_MESSAGE_NO_LOG);
552 else
553 purple_conversation_write_system_message(conv,
554 _("That command only works in IMs, not chats."),
555 PURPLE_MESSAGE_NO_LOG);
556 retval = TRUE;
557 break;
558 case PURPLE_CMD_STATUS_WRONG_PROTOCOL:
559 purple_conversation_write_system_message(conv,
560 _("That command doesn't work on this protocol."),
561 PURPLE_MESSAGE_NO_LOG);
562 retval = TRUE;
563 break;
567 g_free(cmd);
569 return retval;
572 static void
573 send_cb(GtkWidget *widget, PidginConversation *gtkconv)
575 PurpleConversation *conv = gtkconv->active_conv;
576 PurpleAccount *account;
577 PurpleMessageFlags flags = 0;
578 GtkTextBuffer *buffer = NULL;
579 gchar *content;
581 account = purple_conversation_get_account(conv);
583 buffer = talkatu_editor_get_buffer(TALKATU_EDITOR(gtkconv->editor));
585 if (check_for_and_do_command(conv)) {
586 talkatu_buffer_clear(TALKATU_BUFFER(buffer));
587 return;
590 if (PURPLE_IS_CHAT_CONVERSATION(conv) &&
591 purple_chat_conversation_has_left(PURPLE_CHAT_CONVERSATION(conv))) {
592 return;
595 if (!purple_account_is_connected(account)) {
596 return;
599 content = talkatu_markup_get_html(buffer, NULL);
600 if (purple_strequal(content, "")) {
601 g_free(content);
602 return;
605 purple_idle_touch();
607 /* XXX: is there a better way to tell if the message has images? */
608 // if (strstr(buf, "<img ") != NULL)
609 // flags |= PURPLE_MESSAGE_IMAGES;
611 purple_conversation_send_with_flags(conv, content, flags);
613 g_free(content);
615 talkatu_buffer_clear(TALKATU_BUFFER(buffer));
616 gtkconv_set_unseen(gtkconv, PIDGIN_UNSEEN_NONE);
619 static void
620 add_remove_cb(GtkWidget *widget, PidginConversation *gtkconv)
622 PurpleAccount *account;
623 const char *name;
624 PurpleConversation *conv = gtkconv->active_conv;
626 account = purple_conversation_get_account(conv);
627 name = purple_conversation_get_name(conv);
629 if (PURPLE_IS_IM_CONVERSATION(conv)) {
630 PurpleBuddy *b;
632 b = purple_blist_find_buddy(account, name);
633 if (b != NULL)
634 pidgin_dialogs_remove_buddy(b);
635 else if (account != NULL && purple_account_is_connected(account))
636 purple_blist_request_add_buddy(account, (char *)name, NULL, NULL);
637 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
638 PurpleChat *c;
640 c = purple_blist_find_chat(account, name);
641 if (c != NULL)
642 pidgin_dialogs_remove_chat(c);
643 else if (account != NULL && purple_account_is_connected(account))
644 purple_blist_request_add_chat(account, NULL, NULL, name);
648 static void chat_do_info(PidginConversation *gtkconv, const char *who)
650 PurpleChatConversation *chat = PURPLE_CHAT_CONVERSATION(gtkconv->active_conv);
651 PurpleConnection *gc;
653 if ((gc = purple_conversation_get_connection(gtkconv->active_conv))) {
654 pidgin_retrieve_user_info_in_chat(gc, who, purple_chat_conversation_get_id(chat));
659 static void
660 info_cb(GtkWidget *widget, PidginConversation *gtkconv)
662 PurpleConversation *conv = gtkconv->active_conv;
664 if (PURPLE_IS_IM_CONVERSATION(conv)) {
665 pidgin_retrieve_user_info(purple_conversation_get_connection(conv),
666 purple_conversation_get_name(conv));
667 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
668 /* Get info of the person currently selected in the GtkTreeView */
669 PidginChatPane *gtkchat;
670 GtkTreeIter iter;
671 GtkTreeModel *model;
672 GtkTreeSelection *sel;
673 char *name;
675 gtkchat = gtkconv->u.chat;
677 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
678 sel = gtk_tree_view_get_selection(GTK_TREE_VIEW(gtkchat->list));
680 if (gtk_tree_selection_get_selected(sel, NULL, &iter))
681 gtk_tree_model_get(GTK_TREE_MODEL(model), &iter, CHAT_USERS_NAME_COLUMN, &name, -1);
682 else
683 return;
685 chat_do_info(gtkconv, name);
686 g_free(name);
690 static void
691 block_cb(GtkWidget *widget, PidginConversation *gtkconv)
693 PurpleConversation *conv = gtkconv->active_conv;
694 PurpleAccount *account;
696 account = purple_conversation_get_account(conv);
698 if (account != NULL && purple_account_is_connected(account))
699 pidgin_request_add_block(account, purple_conversation_get_name(conv));
702 static void
703 unblock_cb(GtkWidget *widget, PidginConversation *gtkconv)
705 PurpleConversation *conv = gtkconv->active_conv;
706 PurpleAccount *account;
708 account = purple_conversation_get_account(conv);
710 if (account != NULL && purple_account_is_connected(account))
711 pidgin_request_add_permit(account, purple_conversation_get_name(conv));
714 static void
715 do_invite(GtkWidget *w, int resp, gpointer data)
717 PidginInviteDialog *dialog = PIDGIN_INVITE_DIALOG(w);
718 PurpleChatConversation *chat = pidgin_invite_dialog_get_conversation(dialog);
719 const gchar *contact, *message;
721 if (resp == GTK_RESPONSE_ACCEPT) {
722 contact = pidgin_invite_dialog_get_contact(dialog);
723 if (!g_ascii_strcasecmp(contact, ""))
724 return;
726 message = pidgin_invite_dialog_get_message(dialog);
728 purple_serv_chat_invite(purple_conversation_get_connection(PURPLE_CONVERSATION(chat)),
729 purple_chat_conversation_get_id(chat),
730 message, contact);
733 g_clear_pointer(&invite_dialog, gtk_widget_destroy);
736 static void
737 invite_cb(GtkWidget *widget, PidginConversation *gtkconv)
739 PurpleChatConversation *chat = PURPLE_CHAT_CONVERSATION(gtkconv->active_conv);
741 if (invite_dialog == NULL) {
742 invite_dialog = pidgin_invite_dialog_new(chat);
744 /* Connect the signals. */
745 g_signal_connect(G_OBJECT(invite_dialog), "response",
746 G_CALLBACK(do_invite), NULL);
749 gtk_widget_show_all(invite_dialog);
752 static void
753 menu_new_conv_cb(GtkAction *action, gpointer data)
755 pidgin_dialogs_im();
758 static void
759 menu_join_chat_cb(GtkAction *action, gpointer data)
761 pidgin_blist_joinchat_show();
764 static void
765 savelog_writefile_cb(void *user_data, const char *filename)
767 PurpleConversation *conv = (PurpleConversation *)user_data;
768 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
769 GtkTextBuffer *buffer = NULL;
770 FILE *fp;
771 const char *name;
772 gchar *text;
774 if ((fp = g_fopen(filename, "w+")) == NULL) {
775 purple_notify_error(PIDGIN_CONVERSATION(conv), NULL,
776 _("Unable to open file."), NULL,
777 purple_request_cpar_from_conversation(conv));
778 return;
781 name = purple_conversation_get_name(conv);
783 fprintf(fp, "<html>\n");
784 fprintf(fp, "<head>\n");
785 fprintf(fp, "<meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\">\n");
786 fprintf(fp, "<title>%s</title>\n", name);
787 fprintf(fp, "</head>\n");
789 fprintf(fp, "<body>\n");
790 fprintf(fp, _("<h1>Conversation with %s</h1>\n"), name);
791 buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(gtkconv->history));
792 text = talkatu_markup_get_html(buffer, NULL);
793 fprintf(fp, "%s", text);
794 g_free(text);
795 fprintf(fp, "\n</body>\n");
797 fprintf(fp, "</html>\n");
798 fclose(fp);
802 * It would be kinda cool if this gave the option of saving a
803 * plaintext v. HTML file.
805 static void
806 menu_save_as_cb(GtkAction *action, gpointer data)
808 PidginConvWindow *win = data;
809 PurpleConversation *conv = pidgin_conv_window_get_active_conversation(win);
810 PurpleAccount *account = purple_conversation_get_account(conv);
811 PurpleBuddy *buddy = purple_blist_find_buddy(account, purple_conversation_get_name(conv));
812 const char *name;
813 gchar *buf;
814 gchar *c;
816 if (buddy != NULL)
817 name = purple_buddy_get_contact_alias(buddy);
818 else
819 name = purple_normalize(account, purple_conversation_get_name(conv));
821 buf = g_strdup_printf("%s.html", name);
822 for (c = buf ; *c ; c++)
824 if (*c == '/' || *c == '\\')
825 *c = ' ';
827 purple_request_file(PIDGIN_CONVERSATION(conv), _("Save Conversation"),
828 buf, TRUE, G_CALLBACK(savelog_writefile_cb), NULL,
829 purple_request_cpar_from_conversation(conv), conv);
831 g_free(buf);
834 static void
835 menu_view_log_cb(GtkAction *action, gpointer data)
837 PidginConvWindow *win = data;
838 PurpleConversation *conv;
839 PurpleLogType type;
840 PidginBuddyList *gtkblist;
841 const char *name;
842 PurpleAccount *account;
843 GSList *buddies;
844 GSList *cur;
846 conv = pidgin_conv_window_get_active_conversation(win);
848 if (PURPLE_IS_IM_CONVERSATION(conv))
849 type = PURPLE_LOG_IM;
850 else if (PURPLE_IS_CHAT_CONVERSATION(conv))
851 type = PURPLE_LOG_CHAT;
852 else
853 return;
855 gtkblist = pidgin_blist_get_default_gtk_blist();
857 pidgin_set_cursor(gtkblist->window, GDK_WATCH);
858 pidgin_set_cursor(win->window, GDK_WATCH);
860 name = purple_conversation_get_name(conv);
861 account = purple_conversation_get_account(conv);
863 buddies = purple_blist_find_buddies(account, name);
864 for (cur = buddies; cur != NULL; cur = cur->next)
866 PurpleBlistNode *node = cur->data;
867 if ((node != NULL) && ((node->prev != NULL) || (node->next != NULL)))
869 pidgin_log_show_contact((PurpleContact *)node->parent);
870 g_slist_free(buddies);
871 pidgin_clear_cursor(gtkblist->window);
872 pidgin_clear_cursor(win->window);
873 return;
876 g_slist_free(buddies);
878 pidgin_log_show(type, name, account);
880 pidgin_clear_cursor(gtkblist->window);
881 pidgin_clear_cursor(win->window);
884 static void
885 menu_clear_cb(GtkAction *action, gpointer data)
887 PidginConvWindow *win = data;
888 PurpleConversation *conv;
890 conv = pidgin_conv_window_get_active_conversation(win);
891 purple_conversation_clear_message_history(conv);
894 static void
895 menu_find_cb(GtkAction *action, gpointer data)
897 PidginConvWindow *gtkwin = data;
898 PidginConversation *gtkconv = pidgin_conv_window_get_active_gtkconv(gtkwin);
899 gtk_widget_show_all(gtkconv->quickfind_container);
900 gtk_widget_grab_focus(gtkconv->quickfind_entry);
903 #ifdef USE_VV
904 static void
905 menu_initiate_media_call_cb(GtkAction *action, gpointer data)
907 PidginConvWindow *win = (PidginConvWindow *)data;
908 PurpleConversation *conv = pidgin_conv_window_get_active_conversation(win);
909 PurpleAccount *account = purple_conversation_get_account(conv);
911 purple_protocol_initiate_media(account,
912 purple_conversation_get_name(conv),
913 action == win->menu->audio_call ? PURPLE_MEDIA_AUDIO :
914 action == win->menu->video_call ? PURPLE_MEDIA_VIDEO :
915 action == win->menu->audio_video_call ? PURPLE_MEDIA_AUDIO |
916 PURPLE_MEDIA_VIDEO : PURPLE_MEDIA_NONE);
918 #endif
920 static void
921 menu_send_file_cb(GtkAction *action, gpointer data)
923 PidginConvWindow *win = data;
924 PurpleConversation *conv = pidgin_conv_window_get_active_conversation(win);
926 if (PURPLE_IS_IM_CONVERSATION(conv)) {
927 purple_serv_send_file(purple_conversation_get_connection(conv), purple_conversation_get_name(conv), NULL);
932 static void
933 menu_get_attention_cb(GObject *obj, gpointer data)
935 PidginConvWindow *win = data;
936 PurpleConversation *conv = pidgin_conv_window_get_active_conversation(win);
938 if (PURPLE_IS_IM_CONVERSATION(conv)) {
939 int index;
940 if ((GtkAction *)obj == win->menu->get_attention)
941 index = 0;
942 else
943 index = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(obj), "index"));
944 purple_protocol_send_attention(purple_conversation_get_connection(conv),
945 purple_conversation_get_name(conv), index);
949 static void
950 menu_add_pounce_cb(GtkAction *action, gpointer data)
952 PidginConvWindow *win = data;
953 PurpleConversation *conv;
955 conv = pidgin_conv_window_get_active_gtkconv(win)->active_conv;
957 pidgin_pounce_editor_show(purple_conversation_get_account(conv),
958 purple_conversation_get_name(conv), NULL);
961 static void
962 menu_alias_cb(GtkAction *action, gpointer data)
964 PidginConvWindow *win = data;
965 PurpleConversation *conv;
966 PurpleAccount *account;
967 const char *name;
969 conv = pidgin_conv_window_get_active_conversation(win);
970 account = purple_conversation_get_account(conv);
971 name = purple_conversation_get_name(conv);
973 if (PURPLE_IS_IM_CONVERSATION(conv)) {
974 PurpleBuddy *b;
976 b = purple_blist_find_buddy(account, name);
977 if (b != NULL)
978 pidgin_dialogs_alias_buddy(b);
979 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
980 PurpleChat *c;
982 c = purple_blist_find_chat(account, name);
983 if (c != NULL)
984 pidgin_dialogs_alias_chat(c);
988 static void
989 menu_get_info_cb(GtkAction *action, gpointer data)
991 PidginConvWindow *win = data;
992 PurpleConversation *conv;
994 conv = pidgin_conv_window_get_active_conversation(win);
996 info_cb(NULL, PIDGIN_CONVERSATION(conv));
999 static void
1000 menu_invite_cb(GtkAction *action, gpointer data)
1002 PidginConvWindow *win = data;
1003 PurpleConversation *conv;
1005 conv = pidgin_conv_window_get_active_conversation(win);
1007 invite_cb(NULL, PIDGIN_CONVERSATION(conv));
1010 static void
1011 menu_block_cb(GtkAction *action, gpointer data)
1013 PidginConvWindow *win = data;
1014 PurpleConversation *conv;
1016 conv = pidgin_conv_window_get_active_conversation(win);
1018 block_cb(NULL, PIDGIN_CONVERSATION(conv));
1021 static void
1022 menu_unblock_cb(GtkAction *action, gpointer data)
1024 PidginConvWindow *win = data;
1025 PurpleConversation *conv;
1027 conv = pidgin_conv_window_get_active_conversation(win);
1029 unblock_cb(NULL, PIDGIN_CONVERSATION(conv));
1032 static void
1033 menu_add_remove_cb(GtkAction *action, gpointer data)
1035 PidginConvWindow *win = data;
1036 PurpleConversation *conv;
1038 conv = pidgin_conv_window_get_active_conversation(win);
1040 add_remove_cb(NULL, PIDGIN_CONVERSATION(conv));
1043 static gboolean
1044 close_already(gpointer data)
1046 g_object_unref(data);
1047 return FALSE;
1050 static void
1051 hide_conv(PidginConversation *gtkconv, gboolean closetimer)
1053 GList *list;
1055 purple_signal_emit(pidgin_conversations_get_handle(),
1056 "conversation-hiding", gtkconv);
1058 for (list = g_list_copy(gtkconv->convs); list; list = g_list_delete_link(list, list)) {
1059 PurpleConversation *conv = list->data;
1060 if (closetimer) {
1061 guint timer = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(conv), "close-timer"));
1062 if (timer)
1063 g_source_remove(timer);
1064 timer = g_timeout_add_seconds(CLOSE_CONV_TIMEOUT_SECS, close_already, conv);
1065 g_object_set_data(G_OBJECT(conv), "close-timer", GINT_TO_POINTER(timer));
1067 pidgin_conv_window_remove_gtkconv(gtkconv->win, gtkconv);
1068 pidgin_conv_window_add_gtkconv(hidden_convwin, gtkconv);
1072 static void
1073 menu_close_conv_cb(GtkAction *action, gpointer data)
1075 PidginConvWindow *win = data;
1077 close_conv_cb(NULL, PIDGIN_CONVERSATION(pidgin_conv_window_get_active_conversation(win)));
1080 static void
1081 menu_logging_cb(GtkAction *action, gpointer data)
1083 PidginConvWindow *win = data;
1084 PurpleConversation *conv;
1085 gboolean logging;
1086 PurpleBlistNode *node;
1088 conv = pidgin_conv_window_get_active_conversation(win);
1090 if (conv == NULL)
1091 return;
1093 logging = gtk_toggle_action_get_active(GTK_TOGGLE_ACTION(action));
1095 if (logging == purple_conversation_is_logging(conv))
1096 return;
1098 node = get_conversation_blist_node(conv);
1100 if (logging)
1102 /* Enable logging first so the message below can be logged. */
1103 purple_conversation_set_logging(conv, TRUE);
1105 purple_conversation_write_system_message(conv,
1106 _("Logging started. Future messages in this conversation will be logged."), 0);
1108 else
1110 purple_conversation_write_system_message(conv,
1111 _("Logging stopped. Future messages in this conversation will not be logged."), 0);
1113 /* Disable the logging second, so that the above message can be logged. */
1114 purple_conversation_set_logging(conv, FALSE);
1117 /* Save the setting IFF it's different than the pref. */
1118 if (PURPLE_IS_IM_CONVERSATION(conv)) {
1119 if (logging == purple_prefs_get_bool("/purple/logging/log_ims"))
1120 purple_blist_node_remove_setting(node, "enable-logging");
1121 else
1122 purple_blist_node_set_bool(node, "enable-logging", logging);
1123 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
1124 if (logging == purple_prefs_get_bool("/purple/logging/log_chats"))
1125 purple_blist_node_remove_setting(node, "enable-logging");
1126 else
1127 purple_blist_node_set_bool(node, "enable-logging", logging);
1131 static void
1132 menu_toolbar_cb(GtkAction *action, gpointer data)
1134 purple_prefs_set_bool(PIDGIN_PREFS_ROOT "/conversations/show_formatting_toolbar",
1135 gtk_toggle_action_get_active(GTK_TOGGLE_ACTION(action)));
1138 static void
1139 menu_sounds_cb(GtkAction *action, gpointer data)
1141 PidginConvWindow *win = data;
1142 PurpleConversation *conv;
1143 PidginConversation *gtkconv;
1144 PurpleBlistNode *node;
1146 conv = pidgin_conv_window_get_active_conversation(win);
1148 if (!conv)
1149 return;
1151 gtkconv = PIDGIN_CONVERSATION(conv);
1153 gtkconv->make_sound =
1154 gtk_toggle_action_get_active(GTK_TOGGLE_ACTION(action));
1155 node = get_conversation_blist_node(conv);
1156 if (node)
1157 purple_blist_node_set_bool(node, "gtk-mute-sound", !gtkconv->make_sound);
1160 static void
1161 chat_do_im(PidginConversation *gtkconv, const char *who)
1163 PurpleConversation *conv = gtkconv->active_conv;
1164 PurpleAccount *account;
1165 PurpleConnection *gc;
1166 PurpleProtocol *protocol = NULL;
1167 gchar *real_who = NULL;
1169 account = purple_conversation_get_account(conv);
1170 g_return_if_fail(account != NULL);
1172 gc = purple_account_get_connection(account);
1173 g_return_if_fail(gc != NULL);
1175 protocol = purple_connection_get_protocol(gc);
1177 if (protocol)
1178 real_who = purple_protocol_chat_iface_get_user_real_name(protocol, gc,
1179 purple_chat_conversation_get_id(PURPLE_CHAT_CONVERSATION(conv)), who);
1181 if(!who && !real_who)
1182 return;
1184 pidgin_dialogs_im_with_user(account, real_who ? real_who : who);
1186 g_free(real_who);
1189 static void pidgin_conv_chat_update_user(PurpleChatUser *chatuser);
1191 static void
1192 ignore_cb(GtkWidget *w, PidginConversation *gtkconv)
1194 PurpleChatConversation *chat = PURPLE_CHAT_CONVERSATION(gtkconv->active_conv);
1195 const char *name;
1197 name = g_object_get_data(G_OBJECT(w), "user_data");
1199 if (name == NULL)
1200 return;
1202 if (purple_chat_conversation_is_ignored_user(chat, name))
1203 purple_chat_conversation_unignore(chat, name);
1204 else
1205 purple_chat_conversation_ignore(chat, name);
1207 pidgin_conv_chat_update_user(purple_chat_conversation_find_user(chat, name));
1210 static void
1211 menu_chat_im_cb(GtkWidget *w, PidginConversation *gtkconv)
1213 const char *who = g_object_get_data(G_OBJECT(w), "user_data");
1215 chat_do_im(gtkconv, who);
1218 static void
1219 menu_chat_send_file_cb(GtkWidget *w, PidginConversation *gtkconv)
1221 PurpleProtocol *protocol;
1222 PurpleConversation *conv = gtkconv->active_conv;
1223 const char *who = g_object_get_data(G_OBJECT(w), "user_data");
1224 PurpleConnection *gc = purple_conversation_get_connection(conv);
1225 gchar *real_who = NULL;
1227 g_return_if_fail(gc != NULL);
1229 protocol = purple_connection_get_protocol(gc);
1231 if (protocol)
1232 real_who = purple_protocol_chat_iface_get_user_real_name(protocol, gc,
1233 purple_chat_conversation_get_id(PURPLE_CHAT_CONVERSATION(conv)), who);
1235 purple_serv_send_file(gc, real_who ? real_who : who, NULL);
1236 g_free(real_who);
1239 static void
1240 menu_chat_info_cb(GtkWidget *w, PidginConversation *gtkconv)
1242 char *who;
1244 who = g_object_get_data(G_OBJECT(w), "user_data");
1246 chat_do_info(gtkconv, who);
1249 static void
1250 menu_chat_add_remove_cb(GtkWidget *w, PidginConversation *gtkconv)
1252 PurpleConversation *conv = gtkconv->active_conv;
1253 PurpleAccount *account;
1254 PurpleBuddy *b;
1255 char *name;
1257 account = purple_conversation_get_account(conv);
1258 name = g_object_get_data(G_OBJECT(w), "user_data");
1259 b = purple_blist_find_buddy(account, name);
1261 if (b != NULL)
1262 pidgin_dialogs_remove_buddy(b);
1263 else if (account != NULL && purple_account_is_connected(account))
1264 purple_blist_request_add_buddy(account, name, NULL, NULL);
1266 gtk_widget_grab_focus(PIDGIN_CONVERSATION(conv)->entry);
1269 static GtkWidget *
1270 create_chat_menu(PurpleChatConversation *chat, const char *who, PurpleConnection *gc)
1272 static GtkWidget *menu = NULL;
1273 PurpleProtocol *protocol = NULL;
1274 PurpleConversation *conv = PURPLE_CONVERSATION(chat);
1275 PurpleAccount *account = purple_conversation_get_account(conv);
1276 gboolean is_me = FALSE;
1277 GtkWidget *button;
1278 PurpleBuddy *buddy = NULL;
1280 if (gc != NULL)
1281 protocol = purple_connection_get_protocol(gc);
1284 * If a menu already exists, destroy it before creating a new one,
1285 * thus freeing-up the memory it occupied.
1287 if (menu)
1288 gtk_widget_destroy(menu);
1290 if (purple_strequal(purple_chat_conversation_get_nick(chat), purple_normalize(account, who)))
1291 is_me = TRUE;
1293 menu = gtk_menu_new();
1295 if (!is_me) {
1296 button = pidgin_new_menu_item(menu, _("IM"),
1297 PIDGIN_STOCK_TOOLBAR_MESSAGE_NEW,
1298 G_CALLBACK(menu_chat_im_cb),
1299 PIDGIN_CONVERSATION(conv));
1301 if (gc == NULL)
1302 gtk_widget_set_sensitive(button, FALSE);
1303 else
1304 g_object_set_data_full(G_OBJECT(button), "user_data", g_strdup(who), g_free);
1307 if (protocol && PURPLE_IS_PROTOCOL_XFER(protocol))
1309 gboolean can_receive_file = TRUE;
1311 button = pidgin_new_menu_item(menu, _("Send File"),
1312 PIDGIN_STOCK_TOOLBAR_SEND_FILE, G_CALLBACK(menu_chat_send_file_cb),
1313 PIDGIN_CONVERSATION(conv));
1315 if (gc == NULL || protocol == NULL)
1316 can_receive_file = FALSE;
1317 else {
1318 gchar *real_who = NULL;
1319 real_who = purple_protocol_chat_iface_get_user_real_name(protocol, gc,
1320 purple_chat_conversation_get_id(chat), who);
1322 if (!purple_protocol_xfer_can_receive(
1323 PURPLE_PROTOCOL_XFER(protocol),
1324 gc, real_who ? real_who : who)) {
1325 can_receive_file = FALSE;
1328 g_free(real_who);
1331 if (!can_receive_file)
1332 gtk_widget_set_sensitive(button, FALSE);
1333 else
1334 g_object_set_data_full(G_OBJECT(button), "user_data", g_strdup(who), g_free);
1338 if (purple_chat_conversation_is_ignored_user(chat, who))
1339 button = pidgin_new_menu_item(menu, _("Un-Ignore"),
1340 PIDGIN_STOCK_IGNORE, G_CALLBACK(ignore_cb),
1341 PIDGIN_CONVERSATION(conv));
1342 else
1343 button = pidgin_new_menu_item(menu, _("Ignore"),
1344 PIDGIN_STOCK_IGNORE, G_CALLBACK(ignore_cb),
1345 PIDGIN_CONVERSATION(conv));
1347 if (gc == NULL)
1348 gtk_widget_set_sensitive(button, FALSE);
1349 else
1350 g_object_set_data_full(G_OBJECT(button), "user_data", g_strdup(who), g_free);
1353 if (protocol && PURPLE_PROTOCOL_IMPLEMENTS(protocol, SERVER, get_info)) {
1354 button = pidgin_new_menu_item(menu, _("Info"),
1355 PIDGIN_STOCK_TOOLBAR_USER_INFO,
1356 G_CALLBACK(menu_chat_info_cb),
1357 PIDGIN_CONVERSATION(conv));
1359 if (gc == NULL)
1360 gtk_widget_set_sensitive(button, FALSE);
1361 else
1362 g_object_set_data_full(G_OBJECT(button), "user_data", g_strdup(who), g_free);
1365 if (!is_me && protocol && !(purple_protocol_get_options(protocol) & OPT_PROTO_UNIQUE_CHATNAME) && PURPLE_PROTOCOL_IMPLEMENTS(protocol, SERVER, add_buddy)) {
1366 if ((buddy = purple_blist_find_buddy(account, who)) != NULL)
1367 button = pidgin_new_menu_item(menu, _("Remove"),
1368 GTK_STOCK_REMOVE,
1369 G_CALLBACK(menu_chat_add_remove_cb),
1370 PIDGIN_CONVERSATION(conv));
1371 else
1372 button = pidgin_new_menu_item(menu, _("Add"),
1373 GTK_STOCK_ADD,
1374 G_CALLBACK(menu_chat_add_remove_cb),
1375 PIDGIN_CONVERSATION(conv));
1377 if (gc == NULL)
1378 gtk_widget_set_sensitive(button, FALSE);
1379 else
1380 g_object_set_data_full(G_OBJECT(button), "user_data", g_strdup(who), g_free);
1383 if (buddy != NULL)
1385 if (purple_account_is_connected(account))
1386 pidgin_append_blist_node_proto_menu(menu, purple_account_get_connection(account),
1387 (PurpleBlistNode *)buddy);
1388 pidgin_append_blist_node_extended_menu(menu, (PurpleBlistNode *)buddy);
1389 gtk_widget_show_all(menu);
1392 return menu;
1396 static gint
1397 gtkconv_chat_popup_menu_cb(GtkWidget *widget, PidginConversation *gtkconv)
1399 PurpleConversation *conv = gtkconv->active_conv;
1400 PidginChatPane *gtkchat;
1401 PurpleConnection *gc;
1402 PurpleAccount *account;
1403 GtkTreeSelection *sel;
1404 GtkTreeIter iter;
1405 GtkTreeModel *model;
1406 GtkWidget *menu;
1407 gchar *who;
1409 gtkconv = PIDGIN_CONVERSATION(conv);
1410 gtkchat = gtkconv->u.chat;
1411 account = purple_conversation_get_account(conv);
1412 gc = purple_account_get_connection(account);
1414 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
1416 sel = gtk_tree_view_get_selection(GTK_TREE_VIEW(gtkchat->list));
1417 if(!gtk_tree_selection_get_selected(sel, NULL, &iter))
1418 return FALSE;
1420 gtk_tree_model_get(GTK_TREE_MODEL(model), &iter, CHAT_USERS_NAME_COLUMN, &who, -1);
1421 menu = create_chat_menu (PURPLE_CHAT_CONVERSATION(conv), who, gc);
1422 pidgin_menu_popup_at_treeview_selection(menu, widget);
1423 g_free(who);
1425 return TRUE;
1429 static gint
1430 right_click_chat_cb(GtkWidget *widget, GdkEventButton *event,
1431 PidginConversation *gtkconv)
1433 PurpleConversation *conv = gtkconv->active_conv;
1434 PidginChatPane *gtkchat;
1435 PurpleConnection *gc;
1436 PurpleAccount *account;
1437 GtkTreePath *path;
1438 GtkTreeIter iter;
1439 GtkTreeModel *model;
1440 GtkTreeViewColumn *column;
1441 gchar *who;
1442 int x, y;
1444 gtkchat = gtkconv->u.chat;
1445 account = purple_conversation_get_account(conv);
1446 gc = purple_account_get_connection(account);
1448 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
1450 gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(gtkchat->list),
1451 event->x, event->y, &path, &column, &x, &y);
1453 if (path == NULL)
1454 return FALSE;
1456 gtk_tree_selection_select_path(GTK_TREE_SELECTION(
1457 gtk_tree_view_get_selection(GTK_TREE_VIEW(gtkchat->list))), path);
1458 gtk_tree_view_set_cursor(GTK_TREE_VIEW(gtkchat->list),
1459 path, NULL, FALSE);
1460 gtk_widget_grab_focus(GTK_WIDGET(gtkchat->list));
1462 gtk_tree_model_get_iter(GTK_TREE_MODEL(model), &iter, path);
1463 gtk_tree_model_get(GTK_TREE_MODEL(model), &iter, CHAT_USERS_NAME_COLUMN, &who, -1);
1465 /* emit chat-nick-clicked signal */
1466 if (event->type == GDK_BUTTON_PRESS) {
1467 gint plugin_return = GPOINTER_TO_INT(purple_signal_emit_return_1(
1468 pidgin_conversations_get_handle(), "chat-nick-clicked",
1469 conv, who, event->button));
1470 if (plugin_return)
1471 goto handled;
1474 if (event->button == GDK_BUTTON_PRIMARY && event->type == GDK_2BUTTON_PRESS) {
1475 chat_do_im(gtkconv, who);
1476 } else if (gdk_event_triggers_context_menu((GdkEvent *)event)) {
1477 GtkWidget *menu = create_chat_menu (PURPLE_CHAT_CONVERSATION(conv), who, gc);
1478 gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent *)event);
1481 handled:
1482 g_free(who);
1483 gtk_tree_path_free(path);
1485 return TRUE;
1488 static void
1489 activate_list_cb(GtkTreeView *list, GtkTreePath *path, GtkTreeViewColumn *column, PidginConversation *gtkconv)
1491 GtkTreeIter iter;
1492 GtkTreeModel *model;
1493 gchar *who;
1495 model = gtk_tree_view_get_model(GTK_TREE_VIEW(list));
1497 gtk_tree_model_get_iter(GTK_TREE_MODEL(model), &iter, path);
1498 gtk_tree_model_get(GTK_TREE_MODEL(model), &iter, CHAT_USERS_NAME_COLUMN, &who, -1);
1499 chat_do_im(gtkconv, who);
1501 g_free(who);
1504 static void
1505 move_to_next_unread_tab(PidginConversation *gtkconv, gboolean forward)
1507 PidginConversation *next_gtkconv = NULL, *most_active = NULL;
1508 PidginUnseenState unseen_state = PIDGIN_UNSEEN_NONE;
1509 PidginConvWindow *win;
1510 int initial, i, total, diff;
1512 win = gtkconv->win;
1513 initial = gtk_notebook_page_num(GTK_NOTEBOOK(win->notebook),
1514 gtkconv->tab_cont);
1515 total = pidgin_conv_window_get_gtkconv_count(win);
1516 /* By adding total here, the moduli calculated later will always have two
1517 * positive arguments. x % y where x < 0 is not guaranteed to return a
1518 * positive number.
1520 diff = (forward ? 1 : -1) + total;
1522 for (i = (initial + diff) % total; i != initial; i = (i + diff) % total) {
1523 next_gtkconv = pidgin_conv_window_get_gtkconv_at_index(win, i);
1524 if (next_gtkconv->unseen_state > unseen_state) {
1525 most_active = next_gtkconv;
1526 unseen_state = most_active->unseen_state;
1527 if(PIDGIN_UNSEEN_NICK == unseen_state) /* highest possible state */
1528 break;
1532 if (most_active == NULL) { /* no new messages */
1533 i = (i + diff) % total;
1534 most_active = pidgin_conv_window_get_gtkconv_at_index(win, i);
1537 if (most_active != NULL && most_active != gtkconv)
1538 pidgin_conv_window_switch_gtkconv(win, most_active);
1541 static gboolean
1542 gtkconv_cycle_focus(PidginConversation *gtkconv, GtkDirectionType dir)
1544 PurpleConversation *conv = gtkconv->active_conv;
1545 gboolean chat = PURPLE_IS_CHAT_CONVERSATION(conv);
1546 GtkWidget *next = NULL;
1547 struct {
1548 GtkWidget *from;
1549 GtkWidget *to;
1550 } transitions[] = {
1551 {gtkconv->entry, gtkconv->history},
1552 {gtkconv->history, chat ? gtkconv->u.chat->list : gtkconv->entry},
1553 {chat ? gtkconv->u.chat->list : NULL, gtkconv->entry},
1554 {NULL, NULL}
1555 }, *ptr;
1557 for (ptr = transitions; !next && ptr->from; ptr++) {
1558 GtkWidget *from, *to;
1559 if (dir == GTK_DIR_TAB_FORWARD) {
1560 from = ptr->from;
1561 to = ptr->to;
1562 } else {
1563 from = ptr->to;
1564 to = ptr->from;
1566 if (gtk_widget_is_focus(from))
1567 next = to;
1570 if (next)
1571 gtk_widget_grab_focus(next);
1572 return !!next;
1575 static void
1576 update_typing_inserting(PidginConversation *gtkconv)
1578 GtkTextBuffer *buffer = NULL;
1579 gboolean is_empty = FALSE;
1581 g_return_if_fail(gtkconv != NULL);
1583 buffer = talkatu_editor_get_buffer(TALKATU_EDITOR(gtkconv->editor));
1584 is_empty = talkatu_buffer_get_is_empty(TALKATU_BUFFER(buffer));
1586 got_typing_keypress(gtkconv, is_empty);
1589 static gboolean
1590 update_typing_deleting_cb(PidginConversation *gtkconv)
1592 PurpleIMConversation *im = PURPLE_IM_CONVERSATION(gtkconv->active_conv);
1593 GtkTextBuffer *buffer= NULL;
1595 buffer = talkatu_editor_get_buffer(TALKATU_EDITOR(gtkconv->editor));
1597 if (!talkatu_buffer_get_is_empty(buffer)) {
1598 /* We deleted all the text, so turn off typing. */
1599 purple_im_conversation_stop_send_typed_timeout(im);
1601 purple_serv_send_typing(purple_conversation_get_connection(gtkconv->active_conv),
1602 purple_conversation_get_name(gtkconv->active_conv),
1603 PURPLE_IM_NOT_TYPING);
1605 else {
1606 /* We're deleting, but not all of it, so it counts as typing. */
1607 got_typing_keypress(gtkconv, FALSE);
1610 return FALSE;
1613 static void
1614 update_typing_deleting(PidginConversation *gtkconv)
1616 GtkTextBuffer *buffer = NULL;
1618 g_return_if_fail(gtkconv != NULL);
1620 buffer = talkatu_editor_get_buffer(TALKATU_EDITOR(gtkconv->editor));
1622 if (!talkatu_buffer_get_is_empty(TALKATU_BUFFER(buffer))) {
1623 g_timeout_add(0, (GSourceFunc)update_typing_deleting_cb, gtkconv);
1627 static gboolean
1628 conv_keypress_common(PidginConversation *gtkconv, GdkEventKey *event)
1630 PidginConvWindow *win;
1631 int curconv;
1633 win = gtkconv->win;
1634 curconv = gtk_notebook_get_current_page(GTK_NOTEBOOK(win->notebook));
1636 /* clear any tooltips */
1637 pidgin_tooltip_destroy();
1639 /* If CTRL was held down... */
1640 if (event->state & GDK_CONTROL_MASK) {
1641 switch (event->keyval) {
1642 case GDK_KEY_Page_Down:
1643 case GDK_KEY_KP_Page_Down:
1644 case ']':
1645 if (!pidgin_conv_window_get_gtkconv_at_index(win, curconv + 1))
1646 gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook), 0);
1647 else
1648 gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook), curconv + 1);
1649 return TRUE;
1650 break;
1652 case GDK_KEY_Page_Up:
1653 case GDK_KEY_KP_Page_Up:
1654 case '[':
1655 if (!pidgin_conv_window_get_gtkconv_at_index(win, curconv - 1))
1656 gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook), -1);
1657 else
1658 gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook), curconv - 1);
1659 return TRUE;
1660 break;
1662 case GDK_KEY_Tab:
1663 case GDK_KEY_KP_Tab:
1664 case GDK_KEY_ISO_Left_Tab:
1665 if (event->state & GDK_SHIFT_MASK) {
1666 move_to_next_unread_tab(gtkconv, FALSE);
1667 } else {
1668 move_to_next_unread_tab(gtkconv, TRUE);
1671 return TRUE;
1672 break;
1674 case GDK_KEY_comma:
1675 gtk_notebook_reorder_child(GTK_NOTEBOOK(win->notebook),
1676 gtk_notebook_get_nth_page(GTK_NOTEBOOK(win->notebook), curconv),
1677 curconv - 1);
1678 return TRUE;
1679 break;
1681 case GDK_KEY_period:
1682 gtk_notebook_reorder_child(GTK_NOTEBOOK(win->notebook),
1683 gtk_notebook_get_nth_page(GTK_NOTEBOOK(win->notebook), curconv),
1684 (curconv + 1) % gtk_notebook_get_n_pages(GTK_NOTEBOOK(win->notebook)));
1685 return TRUE;
1686 break;
1687 case GDK_KEY_F6:
1688 if (gtkconv_cycle_focus(gtkconv, event->state & GDK_SHIFT_MASK ? GTK_DIR_TAB_BACKWARD : GTK_DIR_TAB_FORWARD))
1689 return TRUE;
1690 break;
1691 } /* End of switch */
1694 /* If ALT (or whatever) was held down... */
1695 else if (event->state & GDK_MOD1_MASK)
1697 if (event->keyval > '0' && event->keyval <= '9')
1699 guint switchto = event->keyval - '1';
1700 if (switchto < pidgin_conv_window_get_gtkconv_count(win))
1701 gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook), switchto);
1703 return TRUE;
1707 /* If neither CTRL nor ALT were held down... */
1708 else
1710 switch (event->keyval) {
1711 case GDK_KEY_F2:
1712 if (gtk_widget_is_focus(GTK_WIDGET(win->notebook))) {
1713 infopane_entry_activate(gtkconv);
1714 return TRUE;
1716 break;
1717 case GDK_KEY_F6:
1718 if (gtkconv_cycle_focus(gtkconv, event->state & GDK_SHIFT_MASK ? GTK_DIR_TAB_BACKWARD : GTK_DIR_TAB_FORWARD))
1719 return TRUE;
1720 break;
1723 return FALSE;
1726 static gboolean
1727 entry_key_press_cb(GtkWidget *entry, GdkEventKey *event, gpointer data)
1729 PurpleConversation *conv;
1730 PidginConversation *gtkconv;
1732 gtkconv = (PidginConversation *)data;
1733 conv = gtkconv->active_conv;
1735 if (conv_keypress_common(gtkconv, event))
1736 return TRUE;
1738 /* If CTRL was held down... */
1739 if (event->state & GDK_CONTROL_MASK) {
1741 /* If ALT (or whatever) was held down... */
1742 else if (event->state & GDK_MOD1_MASK) {
1745 /* If neither CTRL nor ALT were held down... */
1746 else {
1747 switch (event->keyval) {
1748 case GDK_KEY_Tab:
1749 case GDK_KEY_KP_Tab:
1750 case GDK_KEY_ISO_Left_Tab:
1751 if (gtkconv->entry != entry)
1752 break;
1754 gint plugin_return;
1755 plugin_return = GPOINTER_TO_INT(purple_signal_emit_return_1(
1756 pidgin_conversations_get_handle(), "chat-nick-autocomplete",
1757 conv, event->state & GDK_SHIFT_MASK));
1758 return plugin_return;
1760 break;
1762 case GDK_KEY_Page_Up:
1763 case GDK_KEY_KP_Page_Up:
1764 talkatu_history_page_up(TALKATU_HISTORY(gtkconv->history));
1765 return TRUE;
1766 break;
1768 case GDK_KEY_Page_Down:
1769 case GDK_KEY_KP_Page_Down:
1770 talkatu_history_page_down(TALKATU_HISTORY(gtkconv->history));
1771 return TRUE;
1772 break;
1774 case GDK_KEY_KP_Enter:
1775 case GDK_KEY_Return:
1776 send_cb(entry, gtkconv);
1777 return TRUE;
1778 break;
1783 if (PURPLE_IS_IM_CONVERSATION(conv) &&
1784 purple_prefs_get_bool("/purple/conversations/im/send_typing")) {
1786 switch (event->keyval) {
1787 case GDK_KEY_BackSpace:
1788 case GDK_KEY_Delete:
1789 case GDK_KEY_KP_Delete:
1790 update_typing_deleting(gtkconv);
1791 break;
1792 default:
1793 update_typing_inserting(gtkconv);
1797 return FALSE;
1801 * If someone tries to type into the conversation backlog of a
1802 * conversation window then we yank focus from the conversation backlog
1803 * and give it to the text entry box so that people can type
1804 * all the live long day and it will get entered into the entry box.
1806 static gboolean
1807 refocus_entry_cb(GtkWidget *widget, GdkEventKey *event, gpointer data)
1809 GtkWidget *view = NULL;
1810 PidginConversation *gtkconv = data;
1812 /* If we have a valid key for the conversation display, then exit */
1813 if ((event->state & GDK_CONTROL_MASK) ||
1814 (event->keyval == GDK_KEY_F6) ||
1815 (event->keyval == GDK_KEY_F10) ||
1816 (event->keyval == GDK_KEY_Menu) ||
1817 (event->keyval == GDK_KEY_Shift_L) ||
1818 (event->keyval == GDK_KEY_Shift_R) ||
1819 (event->keyval == GDK_KEY_Control_L) ||
1820 (event->keyval == GDK_KEY_Control_R) ||
1821 (event->keyval == GDK_KEY_Escape) ||
1822 (event->keyval == GDK_KEY_Up) ||
1823 (event->keyval == GDK_KEY_Down) ||
1824 (event->keyval == GDK_KEY_Left) ||
1825 (event->keyval == GDK_KEY_Right) ||
1826 (event->keyval == GDK_KEY_Page_Up) ||
1827 (event->keyval == GDK_KEY_KP_Page_Up) ||
1828 (event->keyval == GDK_KEY_Page_Down) ||
1829 (event->keyval == GDK_KEY_KP_Page_Down) ||
1830 (event->keyval == GDK_KEY_Home) ||
1831 (event->keyval == GDK_KEY_End) ||
1832 (event->keyval == GDK_KEY_Tab) ||
1833 (event->keyval == GDK_KEY_KP_Tab) ||
1834 (event->keyval == GDK_KEY_ISO_Left_Tab))
1836 if (event->type == GDK_KEY_PRESS)
1837 return conv_keypress_common(gtkconv, event);
1838 return FALSE;
1841 view = talkatu_editor_get_view(TALKATU_EDITOR(gtkconv->editor));
1842 gtk_widget_grab_focus(view);
1843 gtk_widget_event(view, (GdkEvent *)event);
1845 return TRUE;
1848 static void
1849 regenerate_options_items(PidginConvWindow *win);
1851 void
1852 pidgin_conv_switch_active_conversation(PurpleConversation *conv)
1854 PidginConversation *gtkconv;
1855 PurpleConversation *old_conv;
1856 PurpleConnectionFlags features;
1858 g_return_if_fail(conv != NULL);
1860 gtkconv = PIDGIN_CONVERSATION(conv);
1861 old_conv = gtkconv->active_conv;
1863 purple_debug_info("gtkconv", "setting active conversation on toolbar %p\n",
1864 conv);
1866 if (old_conv == conv)
1867 return;
1869 purple_conversation_close_logs(old_conv);
1870 gtkconv->active_conv = conv;
1872 purple_conversation_set_logging(conv,
1873 gtk_toggle_action_get_active(GTK_TOGGLE_ACTION(gtkconv->win->menu->logging)));
1875 purple_signal_emit(pidgin_conversations_get_handle(), "conversation-switched", conv);
1877 gray_stuff_out(gtkconv);
1878 update_typing_icon(gtkconv);
1879 g_object_set_data(G_OBJECT(gtkconv->entry), "transient_buddy", NULL);
1880 regenerate_options_items(gtkconv->win);
1882 gtk_window_set_title(GTK_WINDOW(gtkconv->win->window),
1883 gtk_label_get_text(GTK_LABEL(gtkconv->tab_label)));
1886 static void
1887 menu_conv_sel_send_cb(GObject *m, gpointer data)
1889 PurpleAccount *account = g_object_get_data(m, "purple_account");
1890 gchar *name = g_object_get_data(m, "purple_buddy_name");
1891 PurpleIMConversation *im;
1893 if (gtk_check_menu_item_get_active((GtkCheckMenuItem*) m) == FALSE)
1894 return;
1896 im = purple_im_conversation_new(account, name);
1897 pidgin_conv_switch_active_conversation(PURPLE_CONVERSATION(im));
1900 /**************************************************************************
1901 * A bunch of buddy icon functions
1902 **************************************************************************/
1904 static GList *get_protocol_icon_list(PurpleAccount *account)
1906 GList *l = NULL;
1907 PurpleProtocol *protocol =
1908 purple_protocols_find(purple_account_get_protocol_id(account));
1909 const char *protoname = purple_protocol_class_list_icon(protocol, account, NULL);
1910 l = g_hash_table_lookup(protocol_lists, protoname);
1911 if (l)
1912 return l;
1914 l = g_list_prepend(l, pidgin_create_protocol_icon(account, PIDGIN_PROTOCOL_ICON_LARGE));
1915 l = g_list_prepend(l, pidgin_create_protocol_icon(account, PIDGIN_PROTOCOL_ICON_MEDIUM));
1916 l = g_list_prepend(l, pidgin_create_protocol_icon(account, PIDGIN_PROTOCOL_ICON_SMALL));
1918 g_hash_table_insert(protocol_lists, g_strdup(protoname), l);
1919 return l;
1922 static GList *
1923 pidgin_conv_get_tab_icons(PurpleConversation *conv)
1925 PurpleAccount *account = NULL;
1926 const char *name = NULL;
1928 g_return_val_if_fail(conv != NULL, NULL);
1930 account = purple_conversation_get_account(conv);
1931 name = purple_conversation_get_name(conv);
1933 g_return_val_if_fail(account != NULL, NULL);
1934 g_return_val_if_fail(name != NULL, NULL);
1936 /* Use the buddy icon, if possible */
1937 if (PURPLE_IS_IM_CONVERSATION(conv)) {
1938 PurpleBuddy *b = purple_blist_find_buddy(account, name);
1939 if (b != NULL) {
1940 PurplePresence *p;
1941 p = purple_buddy_get_presence(b);
1942 if (purple_presence_is_status_primitive_active(p, PURPLE_STATUS_AWAY))
1943 return away_list;
1944 if (purple_presence_is_status_primitive_active(p, PURPLE_STATUS_UNAVAILABLE))
1945 return busy_list;
1946 if (purple_presence_is_status_primitive_active(p, PURPLE_STATUS_EXTENDED_AWAY))
1947 return xa_list;
1948 if (purple_presence_is_status_primitive_active(p, PURPLE_STATUS_OFFLINE))
1949 return offline_list;
1950 else
1951 return available_list;
1955 return get_protocol_icon_list(account);
1958 static const char *
1959 pidgin_conv_get_icon_stock(PurpleConversation *conv)
1961 PurpleAccount *account = NULL;
1962 const char *stock = NULL;
1964 g_return_val_if_fail(conv != NULL, NULL);
1966 account = purple_conversation_get_account(conv);
1967 g_return_val_if_fail(account != NULL, NULL);
1969 /* Use the buddy icon, if possible */
1970 if (PURPLE_IS_IM_CONVERSATION(conv)) {
1971 const char *name = NULL;
1972 PurpleBuddy *b;
1973 name = purple_conversation_get_name(conv);
1974 b = purple_blist_find_buddy(account, name);
1975 if (b != NULL) {
1976 PurplePresence *p = purple_buddy_get_presence(b);
1977 PurpleStatus *active = purple_presence_get_active_status(p);
1978 PurpleStatusType *type = purple_status_get_status_type(active);
1979 PurpleStatusPrimitive prim = purple_status_type_get_primitive(type);
1980 stock = pidgin_stock_id_from_status_primitive(prim);
1981 } else {
1982 stock = PIDGIN_STOCK_STATUS_PERSON;
1984 } else {
1985 stock = PIDGIN_STOCK_STATUS_CHAT;
1988 return stock;
1991 static GdkPixbuf *
1992 pidgin_conv_get_icon(PurpleConversation *conv, GtkWidget *parent, const char *icon_size)
1994 PurpleAccount *account = NULL;
1995 const char *name = NULL;
1996 const char *stock = NULL;
1997 GdkPixbuf *status = NULL;
1998 GtkIconSize size;
2000 g_return_val_if_fail(conv != NULL, NULL);
2002 account = purple_conversation_get_account(conv);
2003 name = purple_conversation_get_name(conv);
2005 g_return_val_if_fail(account != NULL, NULL);
2006 g_return_val_if_fail(name != NULL, NULL);
2008 /* Use the buddy icon, if possible */
2009 if (PURPLE_IS_IM_CONVERSATION(conv)) {
2010 PurpleBuddy *b = purple_blist_find_buddy(account, name);
2011 if (b != NULL) {
2012 /* I hate this hack. It fixes a bug where the pending message icon
2013 * displays in the conv tab even though it shouldn't.
2014 * A better solution would be great. */
2015 purple_blist_update_node(NULL, PURPLE_BLIST_NODE(b));
2019 stock = pidgin_conv_get_icon_stock(conv);
2020 size = gtk_icon_size_from_name(icon_size);
2021 status = gtk_widget_render_icon (parent, stock, size, "GtkWidget");
2022 return status;
2025 GdkPixbuf *
2026 pidgin_conv_get_tab_icon(PurpleConversation *conv, gboolean small_icon)
2028 const char *icon_size = small_icon ? PIDGIN_ICON_SIZE_TANGO_MICROSCOPIC : PIDGIN_ICON_SIZE_TANGO_EXTRA_SMALL;
2029 return pidgin_conv_get_icon(conv, PIDGIN_CONVERSATION(conv)->icon, icon_size);
2033 static void
2034 update_tab_icon(PurpleConversation *conv)
2036 PidginConversation *gtkconv;
2037 PidginConvWindow *win;
2038 GList *l;
2039 GdkPixbuf *emblem = NULL;
2040 const char *status = NULL;
2041 const char *infopane_status = NULL;
2043 g_return_if_fail(conv != NULL);
2045 gtkconv = PIDGIN_CONVERSATION(conv);
2046 win = gtkconv->win;
2047 if (conv != gtkconv->active_conv)
2048 return;
2050 status = infopane_status = pidgin_conv_get_icon_stock(conv);
2052 if (PURPLE_IS_IM_CONVERSATION(conv)) {
2053 PurpleBuddy *b = purple_blist_find_buddy(purple_conversation_get_account(conv), purple_conversation_get_name(conv));
2054 if (b)
2055 emblem = pidgin_blist_get_emblem((PurpleBlistNode*)b);
2058 g_return_if_fail(status != NULL);
2060 g_object_set(G_OBJECT(gtkconv->icon), "stock", status, NULL);
2061 g_object_set(G_OBJECT(gtkconv->menu_icon), "stock", status, NULL);
2063 gtk_list_store_set(GTK_LIST_STORE(gtkconv->infopane_model),
2064 &(gtkconv->infopane_iter),
2065 CONV_ICON_COLUMN, infopane_status, -1);
2067 gtk_list_store_set(GTK_LIST_STORE(gtkconv->infopane_model),
2068 &(gtkconv->infopane_iter),
2069 CONV_EMBLEM_COLUMN, emblem, -1);
2070 if (emblem)
2071 g_object_unref(emblem);
2073 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/blist/show_protocol_icons")) {
2074 emblem = pidgin_create_protocol_icon(purple_conversation_get_account(gtkconv->active_conv), PIDGIN_PROTOCOL_ICON_SMALL);
2075 } else {
2076 emblem = NULL;
2079 gtk_list_store_set(GTK_LIST_STORE(gtkconv->infopane_model),
2080 &(gtkconv->infopane_iter),
2081 CONV_PROTOCOL_ICON_COLUMN, emblem, -1);
2082 if (emblem)
2083 g_object_unref(emblem);
2085 /* XXX seanegan Why do I have to do this? */
2086 gtk_widget_queue_resize(gtkconv->infopane);
2087 gtk_widget_queue_draw(gtkconv->infopane);
2089 if (pidgin_conv_window_is_active_conversation(conv) &&
2090 (!PURPLE_IS_IM_CONVERSATION(conv) || gtkconv->u.im->anim == NULL))
2092 l = pidgin_conv_get_tab_icons(conv);
2094 gtk_window_set_icon_list(GTK_WINDOW(win->window), l);
2098 static gboolean
2099 redraw_icon(gpointer data)
2101 PidginConversation *gtkconv = (PidginConversation *)data;
2102 PurpleConversation *conv = gtkconv->active_conv;
2103 PurpleAccount *account;
2105 GdkPixbuf *buf;
2106 GdkPixbuf *scale;
2107 gint delay;
2108 int scale_width, scale_height;
2109 int size;
2111 gtkconv = PIDGIN_CONVERSATION(conv);
2112 account = purple_conversation_get_account(conv);
2114 if (!(account && purple_account_get_connection(account))) {
2115 gtkconv->u.im->icon_timer = 0;
2116 return FALSE;
2119 gdk_pixbuf_animation_iter_advance(gtkconv->u.im->iter, NULL);
2120 buf = gdk_pixbuf_animation_iter_get_pixbuf(gtkconv->u.im->iter);
2122 scale_width = gdk_pixbuf_get_width(buf);
2123 scale_height = gdk_pixbuf_get_height(buf);
2125 gtk_widget_get_size_request(gtkconv->u.im->icon_container, NULL, &size);
2126 size = MIN(size, MIN(scale_width, scale_height));
2127 size = CLAMP(size, BUDDYICON_SIZE_MIN, BUDDYICON_SIZE_MAX);
2129 if (scale_width == scale_height) {
2130 scale_width = scale_height = size;
2131 } else if (scale_height > scale_width) {
2132 scale_width = size * scale_width / scale_height;
2133 scale_height = size;
2134 } else {
2135 scale_height = size * scale_height / scale_width;
2136 scale_width = size;
2139 scale = gdk_pixbuf_scale_simple(buf, scale_width, scale_height,
2140 GDK_INTERP_BILINEAR);
2141 if (pidgin_gdk_pixbuf_is_opaque(scale))
2142 pidgin_gdk_pixbuf_make_round(scale);
2144 gtk_image_set_from_pixbuf(GTK_IMAGE(gtkconv->u.im->icon), scale);
2145 g_object_unref(G_OBJECT(scale));
2146 gtk_widget_queue_draw(gtkconv->u.im->icon);
2148 delay = gdk_pixbuf_animation_iter_get_delay_time(gtkconv->u.im->iter);
2150 if (delay < 100)
2151 delay = 100;
2153 gtkconv->u.im->icon_timer = g_timeout_add(delay, redraw_icon, gtkconv);
2155 return FALSE;
2158 static void
2159 start_anim(GtkWidget *widget, PidginConversation *gtkconv)
2161 int delay;
2163 if (gtkconv->u.im->anim == NULL)
2164 return;
2166 if (gtkconv->u.im->icon_timer != 0)
2167 return;
2169 if (gdk_pixbuf_animation_is_static_image(gtkconv->u.im->anim))
2170 return;
2172 delay = gdk_pixbuf_animation_iter_get_delay_time(gtkconv->u.im->iter);
2174 if (delay < 100)
2175 delay = 100;
2177 gtkconv->u.im->icon_timer = g_timeout_add(delay, redraw_icon, gtkconv);
2180 static void
2181 remove_icon(GtkWidget *widget, PidginConversation *gtkconv)
2183 GList *children;
2184 GtkWidget *event;
2185 PurpleConversation *conv = gtkconv->active_conv;
2187 g_return_if_fail(conv != NULL);
2189 gtk_widget_set_size_request(gtkconv->u.im->icon_container, -1, BUDDYICON_SIZE_MIN);
2190 children = gtk_container_get_children(GTK_CONTAINER(gtkconv->u.im->icon_container));
2191 if (children) {
2192 /* We know there's only one child here. It'd be nice to shortcut to the
2193 event box, but we can't change the PidginConversation until 3.0 */
2194 event = (GtkWidget *)children->data;
2195 gtk_container_remove(GTK_CONTAINER(gtkconv->u.im->icon_container), event);
2196 g_list_free(children);
2199 if (gtkconv->u.im->anim != NULL)
2200 g_object_unref(G_OBJECT(gtkconv->u.im->anim));
2202 if (gtkconv->u.im->icon_timer != 0)
2203 g_source_remove(gtkconv->u.im->icon_timer);
2205 if (gtkconv->u.im->iter != NULL)
2206 g_object_unref(G_OBJECT(gtkconv->u.im->iter));
2208 gtkconv->u.im->icon_timer = 0;
2209 gtkconv->u.im->icon = NULL;
2210 gtkconv->u.im->anim = NULL;
2211 gtkconv->u.im->iter = NULL;
2212 gtkconv->u.im->show_icon = FALSE;
2215 static void
2216 saveicon_writefile_cb(void *user_data, const char *filename)
2218 PidginConversation *gtkconv = (PidginConversation *)user_data;
2219 PurpleIMConversation *im = PURPLE_IM_CONVERSATION(gtkconv->active_conv);
2220 PurpleBuddyIcon *icon;
2221 const void *data;
2222 size_t len;
2224 icon = purple_im_conversation_get_icon(im);
2225 data = purple_buddy_icon_get_data(icon, &len);
2227 if ((len <= 0) || (data == NULL) || !purple_util_write_data_to_file_absolute(filename, data, len)) {
2228 purple_notify_error(gtkconv, NULL, _("Unable to save icon file to disk."), NULL, NULL);
2232 static void
2233 custom_icon_sel_cb(const char *filename, gpointer data)
2235 if (filename) {
2236 const gchar *name;
2237 PurpleBuddy *buddy;
2238 PurpleContact *contact;
2239 PidginConversation *gtkconv = data;
2240 PurpleConversation *conv = gtkconv->active_conv;
2241 PurpleAccount *account = purple_conversation_get_account(conv);
2243 name = purple_conversation_get_name(conv);
2244 buddy = purple_blist_find_buddy(account, name);
2245 if (!buddy) {
2246 purple_debug_info("custom-icon", "You can only set custom icons for people on your buddylist.\n");
2247 return;
2249 contact = purple_buddy_get_contact(buddy);
2251 purple_buddy_icons_node_set_custom_icon_from_file((PurpleBlistNode*)contact, filename);
2255 static void
2256 set_custom_icon_cb(GtkWidget *widget, PidginConversation *gtkconv)
2258 GtkWidget *win = pidgin_buddy_icon_chooser_new(GTK_WINDOW(gtkconv->win->window),
2259 custom_icon_sel_cb, gtkconv);
2260 gtk_widget_show_all(win);
2263 static void
2264 change_size_cb(GtkWidget *widget, PidginConversation *gtkconv)
2266 int size = 0;
2267 PurpleConversation *conv = gtkconv->active_conv;
2268 GSList *buddies;
2270 gtk_widget_get_size_request(gtkconv->u.im->icon_container, NULL, &size);
2272 if (size == BUDDYICON_SIZE_MAX) {
2273 size = BUDDYICON_SIZE_MIN;
2274 } else {
2275 size = BUDDYICON_SIZE_MAX;
2278 gtk_widget_set_size_request(gtkconv->u.im->icon_container, -1, size);
2279 pidgin_conv_update_buddy_icon(PURPLE_IM_CONVERSATION(conv));
2281 buddies = purple_blist_find_buddies(purple_conversation_get_account(conv),
2282 purple_conversation_get_name(conv));
2283 for (; buddies; buddies = g_slist_delete_link(buddies, buddies)) {
2284 PurpleBuddy *buddy = buddies->data;
2285 PurpleContact *contact = purple_buddy_get_contact(buddy);
2286 purple_blist_node_set_int((PurpleBlistNode*)contact, "pidgin-infopane-iconsize", size);
2290 static void
2291 remove_custom_icon_cb(GtkWidget *widget, PidginConversation *gtkconv)
2293 const gchar *name;
2294 PurpleBuddy *buddy;
2295 PurpleAccount *account;
2296 PurpleContact *contact;
2297 PurpleConversation *conv = gtkconv->active_conv;
2299 account = purple_conversation_get_account(conv);
2300 name = purple_conversation_get_name(conv);
2301 buddy = purple_blist_find_buddy(account, name);
2302 if (!buddy) {
2303 return;
2305 contact = purple_buddy_get_contact(buddy);
2307 purple_buddy_icons_node_set_custom_icon_from_file((PurpleBlistNode*)contact, NULL);
2310 static void
2311 icon_menu_save_cb(GtkWidget *widget, PidginConversation *gtkconv)
2313 PurpleConversation *conv = gtkconv->active_conv;
2314 const gchar *ext;
2315 gchar *buf;
2317 g_return_if_fail(conv != NULL);
2319 ext = purple_buddy_icon_get_extension(purple_im_conversation_get_icon(PURPLE_IM_CONVERSATION(conv)));
2321 buf = g_strdup_printf("%s.%s", purple_normalize(purple_conversation_get_account(conv), purple_conversation_get_name(conv)), ext);
2323 purple_request_file(gtkconv, _("Save Icon"), buf, TRUE,
2324 G_CALLBACK(saveicon_writefile_cb), NULL,
2325 purple_request_cpar_from_conversation(conv), gtkconv);
2327 g_free(buf);
2330 static void
2331 stop_anim(GtkWidget *widget, PidginConversation *gtkconv)
2333 if (gtkconv->u.im->icon_timer != 0)
2334 g_source_remove(gtkconv->u.im->icon_timer);
2336 gtkconv->u.im->icon_timer = 0;
2340 static void
2341 toggle_icon_animate_cb(GtkWidget *w, PidginConversation *gtkconv)
2343 gtkconv->u.im->animate =
2344 gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(w));
2346 if (gtkconv->u.im->animate)
2347 start_anim(NULL, gtkconv);
2348 else
2349 stop_anim(NULL, gtkconv);
2352 static gboolean
2353 icon_menu(GtkWidget *widget, GdkEventButton *e, PidginConversation *gtkconv)
2355 static GtkWidget *menu = NULL;
2356 PurpleConversation *conv;
2357 PurpleBuddy *buddy;
2359 if (e->button == GDK_BUTTON_PRIMARY && e->type == GDK_BUTTON_PRESS) {
2360 change_size_cb(NULL, gtkconv);
2361 return TRUE;
2364 if (!gdk_event_triggers_context_menu((GdkEvent *)e)) {
2365 return FALSE;
2369 * If a menu already exists, destroy it before creating a new one,
2370 * thus freeing-up the memory it occupied.
2372 if (menu != NULL)
2373 gtk_widget_destroy(menu);
2375 menu = gtk_menu_new();
2377 if (gtkconv->u.im->anim &&
2378 !(gdk_pixbuf_animation_is_static_image(gtkconv->u.im->anim)))
2380 pidgin_new_check_item(menu, _("Animate"),
2381 G_CALLBACK(toggle_icon_animate_cb), gtkconv,
2382 gtkconv->u.im->icon_timer);
2385 pidgin_new_menu_item(menu, _("Hide Icon"), NULL,
2386 G_CALLBACK(remove_icon), gtkconv);
2388 pidgin_new_menu_item(menu, _("Save Icon As..."), GTK_STOCK_SAVE_AS,
2389 G_CALLBACK(icon_menu_save_cb), gtkconv);
2391 pidgin_new_menu_item(menu, _("Set Custom Icon..."), NULL,
2392 G_CALLBACK(set_custom_icon_cb), gtkconv);
2394 pidgin_new_menu_item(menu, _("Change Size"), NULL,
2395 G_CALLBACK(change_size_cb), gtkconv);
2397 /* Is there a custom icon for this person? */
2398 conv = gtkconv->active_conv;
2399 buddy = purple_blist_find_buddy(purple_conversation_get_account(conv),
2400 purple_conversation_get_name(conv));
2401 if (buddy)
2403 PurpleContact *contact = purple_buddy_get_contact(buddy);
2404 if (contact && purple_buddy_icons_node_has_custom_icon((PurpleBlistNode*)contact))
2406 pidgin_new_menu_item(menu, _("Remove Custom Icon"),
2407 NULL, G_CALLBACK(remove_custom_icon_cb),
2408 gtkconv);
2412 gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent *)e);
2414 return TRUE;
2417 /**************************************************************************
2418 * End of the bunch of buddy icon functions
2419 **************************************************************************/
2420 void
2421 pidgin_conv_present_conversation(PurpleConversation *conv)
2423 PidginConversation *gtkconv;
2424 GdkModifierType state;
2426 pidgin_conv_attach_to_conversation(conv);
2427 gtkconv = PIDGIN_CONVERSATION(conv);
2429 pidgin_conv_switch_active_conversation(conv);
2430 /* Switch the tab only if the user initiated the event by pressing
2431 * a button or hitting a key. */
2432 if (gtk_get_current_event_state(&state))
2433 pidgin_conv_window_switch_gtkconv(gtkconv->win, gtkconv);
2434 gtk_window_present(GTK_WINDOW(gtkconv->win->window));
2437 static GList *
2438 pidgin_conversations_get_unseen(GList *l,
2439 PidginUnseenState min_state,
2440 gboolean hidden_only,
2441 guint max_count)
2443 GList *r = NULL;
2444 guint c = 0;
2446 for (; l != NULL && (max_count == 0 || c < max_count); l = l->next) {
2447 PurpleConversation *conv = (PurpleConversation*)l->data;
2448 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
2450 if(gtkconv == NULL || gtkconv->active_conv != conv)
2451 continue;
2453 if (gtkconv->unseen_state >= min_state
2454 && (!hidden_only ||
2455 (hidden_only && gtkconv->win == hidden_convwin))) {
2457 r = g_list_prepend(r, conv);
2458 c++;
2462 return r;
2465 GList *
2466 pidgin_conversations_get_unseen_all(PidginUnseenState min_state,
2467 gboolean hidden_only,
2468 guint max_count)
2470 return pidgin_conversations_get_unseen(purple_conversations_get_all(),
2471 min_state, hidden_only, max_count);
2474 GList *
2475 pidgin_conversations_get_unseen_ims(PidginUnseenState min_state,
2476 gboolean hidden_only,
2477 guint max_count)
2479 return pidgin_conversations_get_unseen(purple_conversations_get_ims(),
2480 min_state, hidden_only, max_count);
2483 GList *
2484 pidgin_conversations_get_unseen_chats(PidginUnseenState min_state,
2485 gboolean hidden_only,
2486 guint max_count)
2488 return pidgin_conversations_get_unseen(purple_conversations_get_chats(),
2489 min_state, hidden_only, max_count);
2492 static void
2493 unseen_conv_menu_cb(GtkMenuItem *item, PurpleConversation *conv)
2495 g_return_if_fail(conv != NULL);
2496 pidgin_conv_present_conversation(conv);
2499 static void
2500 unseen_all_conv_menu_cb(GtkMenuItem *item, GList *list)
2502 g_return_if_fail(list != NULL);
2503 /* Do not free the list from here. It will be freed from the
2504 * 'destroy' callback on the menuitem. */
2505 while (list) {
2506 pidgin_conv_present_conversation(list->data);
2507 list = list->next;
2511 guint
2512 pidgin_conversations_fill_menu(GtkWidget *menu, GList *convs)
2514 GList *l;
2515 guint ret=0;
2517 g_return_val_if_fail(menu != NULL, 0);
2518 g_return_val_if_fail(convs != NULL, 0);
2520 for (l = convs; l != NULL ; l = l->next) {
2521 PurpleConversation *conv = (PurpleConversation*)l->data;
2522 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
2524 GtkWidget *icon = gtk_image_new_from_stock(pidgin_conv_get_icon_stock(conv),
2525 gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_MICROSCOPIC));
2526 GtkWidget *item;
2527 gchar *text = g_strdup_printf("%s (%d)",
2528 gtk_label_get_text(GTK_LABEL(gtkconv->tab_label)),
2529 gtkconv->unseen_count);
2531 item = gtk_image_menu_item_new_with_label(text);
2532 gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(item), icon);
2533 g_signal_connect(G_OBJECT(item), "activate", G_CALLBACK(unseen_conv_menu_cb), conv);
2534 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
2535 g_free(text);
2536 ret++;
2539 if (convs->next) {
2540 /* There are more than one conversation. Add an option to show all conversations. */
2541 GtkWidget *item;
2542 GList *list = g_list_copy(convs);
2544 pidgin_separator(menu);
2546 item = gtk_menu_item_new_with_label(_("Show All"));
2547 g_signal_connect(G_OBJECT(item), "activate", G_CALLBACK(unseen_all_conv_menu_cb), list);
2548 g_signal_connect_swapped(G_OBJECT(item), "destroy", G_CALLBACK(g_list_free), list);
2549 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
2552 return ret;
2555 PidginConvWindow *
2556 pidgin_conv_get_window(PidginConversation *gtkconv)
2558 g_return_val_if_fail(gtkconv != NULL, NULL);
2559 return gtkconv->win;
2562 static GtkActionEntry menu_entries[] =
2563 /* TODO: fill out tooltips... */
2565 /* Conversation menu */
2566 { "ConversationMenu", NULL, N_("_Conversation"), NULL, NULL, NULL },
2567 { "NewInstantMessage", PIDGIN_STOCK_TOOLBAR_MESSAGE_NEW, N_("New Instant _Message..."), "<control>M", NULL, G_CALLBACK(menu_new_conv_cb) },
2568 { "JoinAChat", PIDGIN_STOCK_CHAT, N_("Join a _Chat..."), NULL, NULL, G_CALLBACK(menu_join_chat_cb) },
2569 { "Find", GTK_STOCK_FIND, N_("_Find..."), NULL, NULL, G_CALLBACK(menu_find_cb) },
2570 { "ViewLog", NULL, N_("View _Log"), NULL, NULL, G_CALLBACK(menu_view_log_cb) },
2571 { "SaveAs", GTK_STOCK_SAVE_AS, N_("_Save As..."), NULL, NULL, G_CALLBACK(menu_save_as_cb) },
2572 { "ClearScrollback", GTK_STOCK_CLEAR, N_("Clea_r Scrollback"), "<control>L", NULL, G_CALLBACK(menu_clear_cb) },
2574 #ifdef USE_VV
2575 { "MediaMenu", NULL, N_("M_edia"), NULL, NULL, NULL },
2576 { "AudioCall", PIDGIN_STOCK_TOOLBAR_AUDIO_CALL, N_("_Audio Call"), NULL, NULL, G_CALLBACK(menu_initiate_media_call_cb) },
2577 { "VideoCall", PIDGIN_STOCK_TOOLBAR_VIDEO_CALL, N_("_Video Call"), NULL, NULL, G_CALLBACK(menu_initiate_media_call_cb) },
2578 { "AudioVideoCall", PIDGIN_STOCK_TOOLBAR_VIDEO_CALL, N_("Audio/Video _Call"), NULL, NULL, G_CALLBACK(menu_initiate_media_call_cb) },
2579 #endif
2581 { "SendFile", PIDGIN_STOCK_TOOLBAR_SEND_FILE, N_("Se_nd File..."), NULL, NULL, G_CALLBACK(menu_send_file_cb) },
2582 { "GetAttention", PIDGIN_STOCK_TOOLBAR_SEND_ATTENTION, N_("Get _Attention"), NULL, NULL, G_CALLBACK(menu_get_attention_cb) },
2583 { "AddBuddyPounce", NULL, N_("Add Buddy _Pounce..."), NULL, NULL, G_CALLBACK(menu_add_pounce_cb) },
2584 { "GetInfo", PIDGIN_STOCK_TOOLBAR_USER_INFO, N_("_Get Info"), "<control>O", NULL, G_CALLBACK(menu_get_info_cb) },
2585 { "Invite", NULL, N_("In_vite..."), NULL, NULL, G_CALLBACK(menu_invite_cb) },
2586 { "MoreMenu", NULL, N_("M_ore"), NULL, NULL, NULL },
2587 { "Alias", NULL, N_("Al_ias..."), NULL, NULL, G_CALLBACK(menu_alias_cb) },
2588 { "Block", PIDGIN_STOCK_TOOLBAR_BLOCK, N_("_Block..."), NULL, NULL, G_CALLBACK(menu_block_cb) },
2589 { "Unblock", PIDGIN_STOCK_TOOLBAR_UNBLOCK, N_("_Unblock..."), NULL, NULL, G_CALLBACK(menu_unblock_cb) },
2590 { "Add", GTK_STOCK_ADD, N_("_Add..."), NULL, NULL, G_CALLBACK(menu_add_remove_cb) },
2591 { "Remove", GTK_STOCK_REMOVE, N_("_Remove..."), NULL, NULL, G_CALLBACK(menu_add_remove_cb) },
2592 { "InsertLink", PIDGIN_STOCK_TOOLBAR_INSERT_LINK, N_("Insert Lin_k..."), NULL, NULL, NULL },
2593 { "InsertImage", PIDGIN_STOCK_TOOLBAR_INSERT_IMAGE, N_("Insert Imag_e..."), NULL, NULL, NULL },
2594 { "Close", GTK_STOCK_CLOSE, N_("_Close"), "<control>W", NULL, G_CALLBACK(menu_close_conv_cb) },
2596 /* Options */
2597 { "OptionsMenu", NULL, N_("_Options"), NULL, NULL, NULL },
2600 /* Toggle items */
2601 static const GtkToggleActionEntry menu_toggle_entries[] = {
2602 { "EnableLogging", NULL, N_("Enable _Logging"), NULL, NULL, G_CALLBACK(menu_logging_cb), FALSE },
2603 { "EnableSounds", NULL, N_("Enable _Sounds"), NULL, NULL, G_CALLBACK(menu_sounds_cb), FALSE },
2604 { "ShowFormattingToolbars", NULL, N_("Show Formatting _Toolbars"), NULL, NULL, G_CALLBACK(menu_toolbar_cb), FALSE },
2607 static const char *conversation_menu =
2608 "<ui>"
2609 "<menubar name='Conversation'>"
2610 "<menu action='ConversationMenu'>"
2611 "<menuitem action='NewInstantMessage'/>"
2612 "<menuitem action='JoinAChat'/>"
2613 "<separator/>"
2614 "<menuitem action='Find'/>"
2615 "<menuitem action='ViewLog'/>"
2616 "<menuitem action='SaveAs'/>"
2617 "<menuitem action='ClearScrollback'/>"
2618 "<separator/>"
2619 #ifdef USE_VV
2620 "<menu action='MediaMenu'>"
2621 "<menuitem action='AudioCall'/>"
2622 "<menuitem action='VideoCall'/>"
2623 "<menuitem action='AudioVideoCall'/>"
2624 "</menu>"
2625 #endif
2626 "<menuitem action='SendFile'/>"
2627 "<menuitem action='GetAttention'/>"
2628 "<menuitem action='AddBuddyPounce'/>"
2629 "<menuitem action='GetInfo'/>"
2630 "<menuitem action='Invite'/>"
2631 "<menu action='MoreMenu'/>"
2632 "<separator/>"
2633 "<menuitem action='Alias'/>"
2634 "<menuitem action='Block'/>"
2635 "<menuitem action='Unblock'/>"
2636 "<menuitem action='Add'/>"
2637 "<menuitem action='Remove'/>"
2638 "<separator/>"
2639 "<menuitem action='InsertLink'/>"
2640 "<menuitem action='InsertImage'/>"
2641 "<separator/>"
2642 "<menuitem action='Close'/>"
2643 "</menu>"
2644 "<menu action='OptionsMenu'>"
2645 "<menuitem action='EnableLogging'/>"
2646 "<menuitem action='EnableSounds'/>"
2647 "<separator/>"
2648 "<menuitem action='ShowFormattingToolbars'/>"
2649 "</menu>"
2650 "</menubar>"
2651 "</ui>";
2653 static void
2654 sound_method_pref_changed_cb(const char *name, PurplePrefType type,
2655 gconstpointer value, gpointer data)
2657 PidginConvWindow *win = data;
2658 const char *method = value;
2660 if (purple_strequal(method, "none"))
2662 gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(win->menu->sounds),
2663 FALSE);
2664 gtk_action_set_sensitive(win->menu->sounds, FALSE);
2666 else
2668 PidginConversation *gtkconv = pidgin_conv_window_get_active_gtkconv(win);
2670 if (gtkconv != NULL)
2671 gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(win->menu->sounds),
2672 gtkconv->make_sound);
2673 gtk_action_set_sensitive(win->menu->sounds, TRUE);
2677 /* Returns TRUE if some items were added to the menu, FALSE otherwise */
2678 static gboolean
2679 populate_menu_with_options(GtkWidget *menu, PidginConversation *gtkconv, gboolean all)
2681 GList *list;
2682 PurpleConversation *conv;
2683 PurpleAccount *account;
2684 PurpleBlistNode *node = NULL;
2685 PurpleChat *chat = NULL;
2686 PurpleBuddy *buddy = NULL;
2687 gboolean ret;
2689 conv = gtkconv->active_conv;
2690 account = purple_conversation_get_account(conv);
2692 if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
2693 chat = purple_blist_find_chat(account, purple_conversation_get_name(conv));
2695 if ((chat == NULL) && (gtkconv->history != NULL)) {
2696 chat = g_object_get_data(G_OBJECT(gtkconv->history), "transient_chat");
2699 if ((chat == NULL) && (gtkconv->history != NULL)) {
2700 GHashTable *components;
2701 PurpleAccount *account = purple_conversation_get_account(conv);
2702 PurpleProtocol *protocol =
2703 purple_protocols_find(purple_account_get_protocol_id(account));
2704 if (purple_account_get_connection(account) != NULL &&
2705 PURPLE_PROTOCOL_IMPLEMENTS(protocol, CHAT, info_defaults)) {
2706 components = purple_protocol_chat_iface_info_defaults(protocol, purple_account_get_connection(account),
2707 purple_conversation_get_name(conv));
2708 } else {
2709 components = g_hash_table_new_full(g_str_hash, g_str_equal,
2710 g_free, g_free);
2711 g_hash_table_replace(components, g_strdup("channel"),
2712 g_strdup(purple_conversation_get_name(conv)));
2714 chat = purple_chat_new(account, NULL, components);
2715 purple_blist_node_set_transient((PurpleBlistNode *)chat, TRUE);
2716 g_object_set_data_full(G_OBJECT(gtkconv->history), "transient_chat",
2717 chat, (GDestroyNotify)purple_blist_remove_chat);
2719 } else {
2720 if (!purple_account_is_connected(account))
2721 return FALSE;
2723 buddy = purple_blist_find_buddy(account, purple_conversation_get_name(conv));
2724 if (!buddy && gtkconv->history) {
2725 buddy = g_object_get_data(G_OBJECT(gtkconv->history), "transient_buddy");
2727 if (!buddy) {
2728 buddy = purple_buddy_new(account, purple_conversation_get_name(conv), NULL);
2729 purple_blist_node_set_transient((PurpleBlistNode *)buddy, TRUE);
2730 g_object_set_data_full(G_OBJECT(gtkconv->history), "transient_buddy",
2731 buddy, (GDestroyNotify)g_object_unref);
2736 if (chat)
2737 node = (PurpleBlistNode *)chat;
2738 else if (buddy)
2739 node = (PurpleBlistNode *)buddy;
2741 /* Now add the stuff */
2742 if (all) {
2743 if (buddy)
2744 pidgin_blist_make_buddy_menu(menu, buddy, TRUE);
2745 else if (chat) {
2746 /* XXX: */
2748 } else if (node) {
2749 if (purple_account_is_connected(account))
2750 pidgin_append_blist_node_proto_menu(menu, purple_account_get_connection(account), node);
2751 pidgin_append_blist_node_extended_menu(menu, node);
2754 if ((list = gtk_container_get_children(GTK_CONTAINER(menu))) == NULL) {
2755 ret = FALSE;
2756 } else {
2757 g_list_free(list);
2758 ret = TRUE;
2760 return ret;
2763 static void
2764 regenerate_media_items(PidginConvWindow *win)
2766 #ifdef USE_VV
2767 PurpleAccount *account;
2768 PurpleConversation *conv;
2770 conv = pidgin_conv_window_get_active_conversation(win);
2772 if (conv == NULL) {
2773 purple_debug_error("gtkconv", "couldn't get active conversation"
2774 " when regenerating media items\n");
2775 return;
2778 account = purple_conversation_get_account(conv);
2780 if (account == NULL) {
2781 purple_debug_error("gtkconv", "couldn't get account when"
2782 " regenerating media items\n");
2783 return;
2787 * Check if account support voice and/or calls, and
2788 * if the current buddy supports it.
2790 if (account != NULL && PURPLE_IS_IM_CONVERSATION(conv)) {
2791 PurpleMediaCaps caps =
2792 purple_protocol_get_media_caps(account,
2793 purple_conversation_get_name(conv));
2795 gtk_action_set_sensitive(win->menu->audio_call,
2796 caps & PURPLE_MEDIA_CAPS_AUDIO
2797 ? TRUE : FALSE);
2798 gtk_action_set_sensitive(win->menu->video_call,
2799 caps & PURPLE_MEDIA_CAPS_VIDEO
2800 ? TRUE : FALSE);
2801 gtk_action_set_sensitive(win->menu->audio_video_call,
2802 caps & PURPLE_MEDIA_CAPS_AUDIO_VIDEO
2803 ? TRUE : FALSE);
2804 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
2805 /* for now, don't care about chats... */
2806 gtk_action_set_sensitive(win->menu->audio_call, FALSE);
2807 gtk_action_set_sensitive(win->menu->video_call, FALSE);
2808 gtk_action_set_sensitive(win->menu->audio_video_call, FALSE);
2809 } else {
2810 gtk_action_set_sensitive(win->menu->audio_call, FALSE);
2811 gtk_action_set_sensitive(win->menu->video_call, FALSE);
2812 gtk_action_set_sensitive(win->menu->audio_video_call, FALSE);
2814 #endif
2817 static void
2818 regenerate_attention_items(PidginConvWindow *win)
2820 GtkWidget *attention;
2821 GtkWidget *menu;
2822 PurpleConversation *conv;
2823 PurpleConnection *pc;
2824 PurpleProtocol *protocol = NULL;
2825 GList *list;
2827 conv = pidgin_conv_window_get_active_conversation(win);
2828 if (!conv)
2829 return;
2831 attention = gtk_ui_manager_get_widget(win->menu->ui,
2832 "/Conversation/ConversationMenu/GetAttention");
2834 /* Remove the previous entries */
2835 gtk_menu_item_set_submenu(GTK_MENU_ITEM(attention), NULL);
2837 pc = purple_conversation_get_connection(conv);
2838 if (pc != NULL)
2839 protocol = purple_connection_get_protocol(pc);
2841 if (protocol && PURPLE_IS_PROTOCOL_ATTENTION(protocol)) {
2842 list = purple_protocol_attention_get_types(PURPLE_PROTOCOL_ATTENTION(protocol), purple_connection_get_account(pc));
2844 /* Multiple attention types */
2845 if (list && list->next) {
2846 int index = 0;
2848 menu = gtk_menu_new();
2849 while (list) {
2850 PurpleAttentionType *type;
2851 GtkWidget *menuitem;
2853 type = list->data;
2855 menuitem = gtk_menu_item_new_with_label(purple_attention_type_get_name(type));
2856 g_object_set_data(G_OBJECT(menuitem), "index", GINT_TO_POINTER(index));
2857 g_signal_connect(G_OBJECT(menuitem), "activate",
2858 G_CALLBACK(menu_get_attention_cb),
2859 win);
2860 gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
2862 index++;
2863 list = g_list_delete_link(list, list);
2866 gtk_menu_item_set_submenu(GTK_MENU_ITEM(attention), menu);
2867 gtk_widget_show_all(menu);
2872 static void
2873 regenerate_options_items(PidginConvWindow *win)
2875 GtkWidget *menu;
2876 PidginConversation *gtkconv;
2877 GList *list;
2878 GtkWidget *more_menu;
2880 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
2881 more_menu = gtk_ui_manager_get_widget(win->menu->ui,
2882 "/Conversation/ConversationMenu/MoreMenu");
2883 gtk_widget_show(more_menu);
2884 menu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(more_menu));
2886 /* Remove the previous entries */
2887 for (list = gtk_container_get_children(GTK_CONTAINER(menu)); list; )
2889 GtkWidget *w = list->data;
2890 list = g_list_delete_link(list, list);
2891 gtk_widget_destroy(w);
2894 if (!populate_menu_with_options(menu, gtkconv, FALSE))
2896 GtkWidget *item = gtk_menu_item_new_with_label(_("No actions available"));
2897 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
2898 gtk_widget_set_sensitive(item, FALSE);
2901 gtk_widget_show_all(menu);
2904 static void
2905 remove_from_list(GtkWidget *widget, PidginConvWindow *win)
2907 GList *list = g_object_get_data(G_OBJECT(win->window), "plugin-actions");
2908 list = g_list_remove(list, widget);
2909 g_object_set_data(G_OBJECT(win->window), "plugin-actions", list);
2912 static void
2913 regenerate_plugins_items(PidginConvWindow *win)
2915 GList *action_items;
2916 GtkWidget *menu;
2917 GList *list;
2918 PidginConversation *gtkconv;
2919 PurpleConversation *conv;
2920 GtkWidget *item;
2922 if (win->window == NULL || win == hidden_convwin)
2923 return;
2925 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
2926 if (gtkconv == NULL)
2927 return;
2929 conv = gtkconv->active_conv;
2930 action_items = g_object_get_data(G_OBJECT(win->window), "plugin-actions");
2932 /* Remove the old menuitems */
2933 while (action_items) {
2934 g_signal_handlers_disconnect_by_func(G_OBJECT(action_items->data),
2935 G_CALLBACK(remove_from_list), win);
2936 gtk_widget_destroy(action_items->data);
2937 action_items = g_list_delete_link(action_items, action_items);
2940 item = gtk_ui_manager_get_widget(win->menu->ui, "/Conversation/OptionsMenu");
2941 menu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(item));
2943 list = purple_conversation_get_extended_menu(conv);
2944 if (list) {
2945 action_items = g_list_prepend(NULL, (item = pidgin_separator(menu)));
2946 g_signal_connect(G_OBJECT(item), "destroy", G_CALLBACK(remove_from_list), win);
2949 for(; list; list = g_list_delete_link(list, list)) {
2950 PurpleActionMenu *act = (PurpleActionMenu *) list->data;
2951 item = pidgin_append_menu_action(menu, act, conv);
2952 action_items = g_list_prepend(action_items, item);
2953 gtk_widget_show_all(item);
2954 g_signal_connect(G_OBJECT(item), "destroy", G_CALLBACK(remove_from_list), win);
2956 g_object_set_data(G_OBJECT(win->window), "plugin-actions", action_items);
2959 static void menubar_activated(GtkWidget *item, gpointer data)
2961 PidginConvWindow *win = data;
2962 regenerate_media_items(win);
2963 regenerate_options_items(win);
2964 regenerate_plugins_items(win);
2965 regenerate_attention_items(win);
2967 /* The following are to make sure the 'More' submenu is not regenerated every time
2968 * the focus shifts from 'Conversations' to some other menu and back. */
2969 g_signal_handlers_block_by_func(G_OBJECT(item), G_CALLBACK(menubar_activated), data);
2970 g_signal_connect(G_OBJECT(win->menu->menubar), "deactivate", G_CALLBACK(focus_out_from_menubar), data);
2973 static void
2974 focus_out_from_menubar(GtkWidget *wid, PidginConvWindow *win)
2976 /* The menubar has been deactivated. Make sure the 'More' submenu is regenerated next time
2977 * the 'Conversation' menu pops up. */
2978 GtkWidget *menuitem = gtk_ui_manager_get_widget(win->menu->ui, "/Conversation/ConversationMenu");
2979 g_signal_handlers_unblock_by_func(G_OBJECT(menuitem), G_CALLBACK(menubar_activated), win);
2980 g_signal_handlers_disconnect_by_func(G_OBJECT(win->menu->menubar),
2981 G_CALLBACK(focus_out_from_menubar), win);
2984 static GtkWidget *
2985 setup_menubar(PidginConvWindow *win)
2987 GtkAccelGroup *accel_group;
2988 const char *method;
2989 GtkActionGroup *action_group;
2990 GError *error;
2991 GtkWidget *menuitem;
2993 action_group = gtk_action_group_new("ConversationActions");
2994 gtk_action_group_set_translation_domain(action_group, PACKAGE);
2995 gtk_action_group_add_actions(action_group,
2996 menu_entries,
2997 G_N_ELEMENTS(menu_entries),
2998 win);
2999 gtk_action_group_add_toggle_actions(action_group,
3000 menu_toggle_entries,
3001 G_N_ELEMENTS(menu_toggle_entries),
3002 win);
3004 win->menu->ui = gtk_ui_manager_new();
3005 gtk_ui_manager_insert_action_group(win->menu->ui, action_group, 0);
3007 accel_group = gtk_ui_manager_get_accel_group(win->menu->ui);
3008 gtk_window_add_accel_group(GTK_WINDOW(win->window), accel_group);
3009 g_signal_connect(G_OBJECT(accel_group), "accel-changed",
3010 G_CALLBACK(pidgin_save_accels_cb), NULL);
3012 error = NULL;
3013 if (!gtk_ui_manager_add_ui_from_string(win->menu->ui, conversation_menu, -1, &error))
3015 g_message("building menus failed: %s", error->message);
3016 g_error_free(error);
3017 exit(EXIT_FAILURE);
3020 win->menu->menubar =
3021 gtk_ui_manager_get_widget(win->menu->ui, "/Conversation");
3023 /* Make sure the 'Conversation ⇨ More' menuitems are regenerated whenever
3024 * the 'Conversation' menu pops up because the entries can change after the
3025 * conversation is created. */
3026 menuitem = gtk_ui_manager_get_widget(win->menu->ui, "/Conversation/ConversationMenu");
3027 g_signal_connect(G_OBJECT(menuitem), "activate", G_CALLBACK(menubar_activated), win);
3029 win->menu->view_log =
3030 gtk_ui_manager_get_action(win->menu->ui,
3031 "/Conversation/ConversationMenu/ViewLog");
3033 #ifdef USE_VV
3034 win->menu->audio_call =
3035 gtk_ui_manager_get_action(win->menu->ui,
3036 "/Conversation/ConversationMenu/MediaMenu/AudioCall");
3037 win->menu->video_call =
3038 gtk_ui_manager_get_action(win->menu->ui,
3039 "/Conversation/ConversationMenu/MediaMenu/VideoCall");
3040 win->menu->audio_video_call =
3041 gtk_ui_manager_get_action(win->menu->ui,
3042 "/Conversation/ConversationMenu/MediaMenu/AudioVideoCall");
3043 #endif
3045 /* --- */
3047 win->menu->send_file =
3048 gtk_ui_manager_get_action(win->menu->ui,
3049 "/Conversation/ConversationMenu/SendFile");
3051 win->menu->get_attention =
3052 gtk_ui_manager_get_action(win->menu->ui,
3053 "/Conversation/ConversationMenu/GetAttention");
3055 win->menu->add_pounce =
3056 gtk_ui_manager_get_action(win->menu->ui,
3057 "/Conversation/ConversationMenu/AddBuddyPounce");
3059 /* --- */
3061 win->menu->get_info =
3062 gtk_ui_manager_get_action(win->menu->ui,
3063 "/Conversation/ConversationMenu/GetInfo");
3065 win->menu->invite =
3066 gtk_ui_manager_get_action(win->menu->ui,
3067 "/Conversation/ConversationMenu/Invite");
3069 /* --- */
3071 win->menu->alias =
3072 gtk_ui_manager_get_action(win->menu->ui,
3073 "/Conversation/ConversationMenu/Alias");
3075 win->menu->block =
3076 gtk_ui_manager_get_action(win->menu->ui,
3077 "/Conversation/ConversationMenu/Block");
3079 win->menu->unblock =
3080 gtk_ui_manager_get_action(win->menu->ui,
3081 "/Conversation/ConversationMenu/Unblock");
3083 win->menu->add =
3084 gtk_ui_manager_get_action(win->menu->ui,
3085 "/Conversation/ConversationMenu/Add");
3087 win->menu->remove =
3088 gtk_ui_manager_get_action(win->menu->ui,
3089 "/Conversation/ConversationMenu/Remove");
3091 /* --- */
3093 win->menu->insert_link =
3094 gtk_ui_manager_get_action(win->menu->ui,
3095 "/Conversation/ConversationMenu/InsertLink");
3097 win->menu->insert_image =
3098 gtk_ui_manager_get_action(win->menu->ui,
3099 "/Conversation/ConversationMenu/InsertImage");
3101 /* --- */
3103 win->menu->logging =
3104 gtk_ui_manager_get_action(win->menu->ui,
3105 "/Conversation/OptionsMenu/EnableLogging");
3106 win->menu->sounds =
3107 gtk_ui_manager_get_action(win->menu->ui,
3108 "/Conversation/OptionsMenu/EnableSounds");
3109 method = purple_prefs_get_string(PIDGIN_PREFS_ROOT "/sound/method");
3110 if (purple_strequal(method, "none"))
3112 gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(win->menu->sounds),
3113 FALSE);
3114 gtk_action_set_sensitive(win->menu->sounds, FALSE);
3116 purple_prefs_connect_callback(win, PIDGIN_PREFS_ROOT "/sound/method",
3117 sound_method_pref_changed_cb, win);
3119 win->menu->show_formatting_toolbar =
3120 gtk_ui_manager_get_action(win->menu->ui,
3121 "/Conversation/OptionsMenu/ShowFormattingToolbars");
3123 win->menu->tray = pidgin_menu_tray_new();
3124 gtk_menu_shell_append(GTK_MENU_SHELL(win->menu->menubar),
3125 win->menu->tray);
3126 gtk_widget_show(win->menu->tray);
3128 gtk_widget_show(win->menu->menubar);
3130 return win->menu->menubar;
3134 /**************************************************************************
3135 * Utility functions
3136 **************************************************************************/
3138 static void
3139 got_typing_keypress(PidginConversation *gtkconv, gboolean first)
3141 PurpleConversation *conv = gtkconv->active_conv;
3142 PurpleIMConversation *im;
3145 * We know we got something, so we at least have to make sure we don't
3146 * send PURPLE_IM_TYPED any time soon.
3149 im = PURPLE_IM_CONVERSATION(conv);
3151 purple_im_conversation_stop_send_typed_timeout(im);
3152 purple_im_conversation_start_send_typed_timeout(im);
3154 /* Check if we need to send another PURPLE_IM_TYPING message */
3155 if (first || (purple_im_conversation_get_type_again(im) != 0 &&
3156 time(NULL) > purple_im_conversation_get_type_again(im)))
3158 unsigned int timeout;
3159 timeout = purple_serv_send_typing(purple_conversation_get_connection(conv),
3160 purple_conversation_get_name(conv),
3161 PURPLE_IM_TYPING);
3162 purple_im_conversation_set_type_again(im, timeout);
3166 #if 0
3167 static gboolean
3168 typing_animation(gpointer data) {
3169 PidginConversation *gtkconv = data;
3170 PidginConvWindow *gtkwin = gtkconv->win;
3171 const char *stock_id = NULL;
3173 if(gtkconv != pidgin_conv_window_get_active_gtkconv(gtkwin)) {
3174 return FALSE;
3177 switch (rand() % 5) {
3178 case 0:
3179 stock_id = PIDGIN_STOCK_ANIMATION_TYPING0;
3180 break;
3181 case 1:
3182 stock_id = PIDGIN_STOCK_ANIMATION_TYPING1;
3183 break;
3184 case 2:
3185 stock_id = PIDGIN_STOCK_ANIMATION_TYPING2;
3186 break;
3187 case 3:
3188 stock_id = PIDGIN_STOCK_ANIMATION_TYPING3;
3189 break;
3190 case 4:
3191 stock_id = PIDGIN_STOCK_ANIMATION_TYPING4;
3192 break;
3194 if (gtkwin->menu->typing_icon == NULL) {
3195 gtkwin->menu->typing_icon = gtk_image_new_from_stock(stock_id, GTK_ICON_SIZE_MENU);
3196 pidgin_menu_tray_append(PIDGIN_MENU_TRAY(gtkwin->menu->tray),
3197 gtkwin->menu->typing_icon,
3198 _("User is typing..."));
3199 } else {
3200 gtk_image_set_from_stock(GTK_IMAGE(gtkwin->menu->typing_icon), stock_id, GTK_ICON_SIZE_MENU);
3202 gtk_widget_show(gtkwin->menu->typing_icon);
3203 return TRUE;
3205 #endif
3207 static void
3208 update_typing_message(PidginConversation *gtkconv, const char *message)
3210 /* TODO WEBKIT: this is not handled at all */
3211 #if 0
3212 GtkTextBuffer *buffer;
3213 GtkTextMark *stmark, *enmark;
3215 if (g_object_get_data(G_OBJECT(gtkconv->imhtml), "disable-typing-notification"))
3216 return;
3218 buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(gtkconv->imhtml));
3219 stmark = gtk_text_buffer_get_mark(buffer, "typing-notification-start");
3220 enmark = gtk_text_buffer_get_mark(buffer, "typing-notification-end");
3221 if (stmark && enmark) {
3222 GtkTextIter start, end;
3223 gtk_text_buffer_get_iter_at_mark(buffer, &start, stmark);
3224 gtk_text_buffer_get_iter_at_mark(buffer, &end, enmark);
3225 gtk_text_buffer_delete_mark(buffer, stmark);
3226 gtk_text_buffer_delete_mark(buffer, enmark);
3227 gtk_text_buffer_delete(buffer, &start, &end);
3228 } else if (message && *message == '\n' && message[1] == ' ' && message[2] == '\0')
3229 message = NULL;
3231 #ifdef RESERVE_LINE
3232 if (!message)
3233 message = "\n "; /* The blank space is required to avoid a GTK+/Pango bug */
3234 #endif
3236 if (message) {
3237 GtkTextIter iter;
3238 gtk_text_buffer_get_end_iter(buffer, &iter);
3239 gtk_text_buffer_create_mark(buffer, "typing-notification-start", &iter, TRUE);
3240 gtk_text_buffer_insert_with_tags_by_name(buffer, &iter, message, -1, "TYPING-NOTIFICATION", NULL);
3241 gtk_text_buffer_get_end_iter(buffer, &iter);
3242 gtk_text_buffer_create_mark(buffer, "typing-notification-end", &iter, TRUE);
3244 #endif /* if 0 */
3247 static void
3248 update_typing_icon(PidginConversation *gtkconv)
3250 PurpleIMConversation *im;
3251 char *message = NULL;
3253 if (!PURPLE_IS_IM_CONVERSATION(gtkconv->active_conv))
3254 return;
3256 im = PURPLE_IM_CONVERSATION(gtkconv->active_conv);
3258 if (purple_im_conversation_get_typing_state(im) == PURPLE_IM_NOT_TYPING) {
3259 #ifdef RESERVE_LINE
3260 update_typing_message(gtkconv, NULL);
3261 #else
3262 update_typing_message(gtkconv, "\n ");
3263 #endif
3264 return;
3267 if (purple_im_conversation_get_typing_state(im) == PURPLE_IM_TYPING) {
3268 message = g_strdup_printf(_("\n%s is typing..."), purple_conversation_get_title(PURPLE_CONVERSATION(im)));
3269 } else {
3270 message = g_strdup_printf(_("\n%s has stopped typing"), purple_conversation_get_title(PURPLE_CONVERSATION(im)));
3273 update_typing_message(gtkconv, message);
3274 g_free(message);
3277 static gboolean
3278 update_send_to_selection(PidginConvWindow *win)
3280 PurpleAccount *account;
3281 PurpleConversation *conv;
3282 GtkWidget *menu;
3283 GList *child;
3284 PurpleBuddy *b;
3286 conv = pidgin_conv_window_get_active_conversation(win);
3288 if (conv == NULL)
3289 return FALSE;
3291 account = purple_conversation_get_account(conv);
3293 if (account == NULL)
3294 return FALSE;
3296 if (win->menu->send_to == NULL)
3297 return FALSE;
3299 if (!(b = purple_blist_find_buddy(account, purple_conversation_get_name(conv))))
3300 return FALSE;
3302 gtk_widget_show(win->menu->send_to);
3304 menu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(win->menu->send_to));
3306 for (child = gtk_container_get_children(GTK_CONTAINER(menu));
3307 child != NULL;
3308 child = g_list_delete_link(child, child)) {
3310 GtkWidget *item = child->data;
3311 PurpleBuddy *item_buddy;
3312 PurpleAccount *item_account = g_object_get_data(G_OBJECT(item), "purple_account");
3313 gchar *buddy_name = g_object_get_data(G_OBJECT(item),
3314 "purple_buddy_name");
3315 item_buddy = purple_blist_find_buddy(item_account, buddy_name);
3317 if (b == item_buddy) {
3318 gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item), TRUE);
3319 g_list_free(child);
3320 break;
3324 return FALSE;
3327 static gboolean
3328 send_to_item_enter_notify_cb(GtkWidget *menuitem, GdkEventCrossing *event, GtkWidget *label)
3330 gtk_widget_set_sensitive(GTK_WIDGET(label), TRUE);
3331 return FALSE;
3334 static gboolean
3335 send_to_item_leave_notify_cb(GtkWidget *menuitem, GdkEventCrossing *event, GtkWidget *label)
3337 gtk_widget_set_sensitive(GTK_WIDGET(label), FALSE);
3338 return FALSE;
3341 static GtkWidget *
3342 e2ee_state_to_gtkimage(PurpleE2eeState *state)
3344 PurpleImage *img;
3346 img = _pidgin_e2ee_stock_icon_get(
3347 purple_e2ee_state_get_stock_icon(state));
3348 if (!img)
3349 return NULL;
3351 return gtk_image_new_from_pixbuf(pidgin_pixbuf_from_image(img));
3354 static void
3355 create_sendto_item(GtkWidget *menu, GtkSizeGroup *sg, GSList **group,
3356 PurpleBuddy *buddy, PurpleAccount *account, const char *name,
3357 gboolean e2ee_enabled)
3359 GtkWidget *box;
3360 GtkWidget *label;
3361 GtkWidget *image;
3362 GtkWidget *e2ee_image = NULL;
3363 GtkWidget *menuitem;
3364 GdkPixbuf *pixbuf;
3365 gchar *text;
3367 /* Create a pixmap for the protocol icon. */
3368 pixbuf = pidgin_create_protocol_icon(account, PIDGIN_PROTOCOL_ICON_SMALL);
3370 /* Now convert it to GtkImage */
3371 if (pixbuf == NULL)
3372 image = gtk_image_new();
3373 else
3375 image = gtk_image_new_from_pixbuf(pixbuf);
3376 g_object_unref(G_OBJECT(pixbuf));
3379 if (e2ee_enabled) {
3380 PurpleIMConversation *im;
3381 PurpleE2eeState *state = NULL;
3383 im = purple_conversations_find_im_with_account(
3384 purple_buddy_get_name(buddy), purple_buddy_get_account(buddy));
3385 if (im)
3386 state = purple_conversation_get_e2ee_state(PURPLE_CONVERSATION(im));
3387 if (state)
3388 e2ee_image = e2ee_state_to_gtkimage(state);
3389 else
3390 e2ee_image = gtk_image_new();
3393 gtk_size_group_add_widget(sg, image);
3395 /* Make our menu item */
3396 text = g_strdup_printf("%s (%s)", name, purple_account_get_name_for_display(account));
3397 menuitem = gtk_radio_menu_item_new_with_label(*group, text);
3398 g_free(text);
3399 *group = gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(menuitem));
3401 /* Do some evil, see some evil, speak some evil. */
3402 box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
3404 label = gtk_bin_get_child(GTK_BIN(menuitem));
3405 g_object_ref(label);
3406 gtk_container_remove(GTK_CONTAINER(menuitem), label);
3408 gtk_box_pack_start(GTK_BOX(box), image, FALSE, FALSE, 0);
3410 gtk_box_pack_start(GTK_BOX(box), label, TRUE, TRUE, 4);
3411 if (e2ee_image)
3412 gtk_box_pack_start(GTK_BOX(box), e2ee_image, FALSE, FALSE, 0);
3414 if (buddy != NULL &&
3415 !purple_presence_is_online(purple_buddy_get_presence(buddy)))
3417 gtk_widget_set_sensitive(label, FALSE);
3419 /* Set the label sensitive when the menuitem is highlighted and
3420 * insensitive again when the mouse leaves it. This way, it
3421 * doesn't appear weird from the highlighting of the embossed
3422 * (insensitive style) text.*/
3423 g_signal_connect(menuitem, "enter-notify-event",
3424 G_CALLBACK(send_to_item_enter_notify_cb), label);
3425 g_signal_connect(menuitem, "leave-notify-event",
3426 G_CALLBACK(send_to_item_leave_notify_cb), label);
3429 g_object_unref(label);
3431 gtk_container_add(GTK_CONTAINER(menuitem), box);
3433 gtk_widget_show(label);
3434 gtk_widget_show(image);
3435 if (e2ee_image)
3436 gtk_widget_show(e2ee_image);
3437 gtk_widget_show(box);
3439 /* Set our data and callbacks. */
3440 g_object_set_data(G_OBJECT(menuitem), "purple_account", account);
3441 g_object_set_data_full(G_OBJECT(menuitem), "purple_buddy_name", g_strdup(name), g_free);
3443 g_signal_connect(G_OBJECT(menuitem), "activate",
3444 G_CALLBACK(menu_conv_sel_send_cb), NULL);
3446 gtk_widget_show(menuitem);
3447 gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
3450 static gboolean
3451 compare_buddy_presence(PurplePresence *p1, PurplePresence *p2)
3453 /* This is necessary because multiple PurpleBuddy's don't share the same
3454 * PurplePresence anymore.
3456 PurpleBuddy *b1 = purple_buddy_presence_get_buddy(PURPLE_BUDDY_PRESENCE(p1));
3457 PurpleBuddy *b2 = purple_buddy_presence_get_buddy(PURPLE_BUDDY_PRESENCE(p2));
3458 if (purple_buddy_get_account(b1) == purple_buddy_get_account(b2) &&
3459 purple_strequal(purple_buddy_get_name(b1), purple_buddy_get_name(b2)))
3460 return FALSE;
3461 return TRUE;
3464 static void
3465 generate_send_to_items(PidginConvWindow *win)
3467 GtkWidget *menu;
3468 GSList *group = NULL;
3469 GtkSizeGroup *sg = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL);
3470 PidginConversation *gtkconv;
3471 GSList *l, *buds;
3473 g_return_if_fail(win != NULL);
3475 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
3477 g_return_if_fail(gtkconv != NULL);
3479 if (win->menu->send_to != NULL)
3480 gtk_widget_destroy(win->menu->send_to);
3482 /* Build the Send To menu */
3483 win->menu->send_to = gtk_menu_item_new_with_mnemonic(_("S_end To"));
3484 gtk_widget_show(win->menu->send_to);
3486 menu = gtk_menu_new();
3487 gtk_menu_shell_insert(GTK_MENU_SHELL(win->menu->menubar),
3488 win->menu->send_to, 2);
3489 gtk_menu_item_set_submenu(GTK_MENU_ITEM(win->menu->send_to), menu);
3491 gtk_widget_show(menu);
3493 if (PURPLE_IS_IM_CONVERSATION(gtkconv->active_conv)) {
3494 buds = purple_blist_find_buddies(purple_conversation_get_account(gtkconv->active_conv), purple_conversation_get_name(gtkconv->active_conv));
3496 if (buds == NULL)
3498 /* The user isn't on the buddy list. So we don't create any sendto menu. */
3500 else
3502 gboolean e2ee_enabled = FALSE;
3503 GList *list = NULL, *iter;
3504 for (l = buds; l != NULL; l = l->next)
3506 PurpleBlistNode *node;
3508 node = PURPLE_BLIST_NODE(purple_buddy_get_contact(PURPLE_BUDDY(l->data)));
3510 for (node = node->child; node != NULL; node = node->next)
3512 PurpleBuddy *buddy = (PurpleBuddy *)node;
3513 PurpleAccount *account;
3514 PurpleIMConversation *im;
3516 if (!PURPLE_IS_BUDDY(node))
3517 continue;
3519 im = purple_conversations_find_im_with_account(purple_buddy_get_name(buddy), purple_buddy_get_account(buddy));
3520 if (im && purple_conversation_get_e2ee_state(PURPLE_CONVERSATION(im)) != NULL)
3521 e2ee_enabled = TRUE;
3523 account = purple_buddy_get_account(buddy);
3524 /* TODO WEBKIT: (I'm not actually sure if this is webkit-related --Mark Doliner) */
3525 if (purple_account_is_connected(account) /*|| account == purple_conversation_get_account(gtkconv->active_conv)*/)
3527 /* Use the PurplePresence to get unique buddies. */
3528 PurplePresence *presence = purple_buddy_get_presence(buddy);
3529 if (!g_list_find_custom(list, presence, (GCompareFunc)compare_buddy_presence))
3530 list = g_list_prepend(list, presence);
3535 /* Create the sendto menu only if it has more than one item to show */
3536 if (list && list->next) {
3537 /* Loop over the list backwards so we get the items in the right order,
3538 * since we did a g_list_prepend() earlier. */
3539 for (iter = g_list_last(list); iter != NULL; iter = iter->prev) {
3540 PurplePresence *pre = iter->data;
3541 PurpleBuddy *buddy = purple_buddy_presence_get_buddy(PURPLE_BUDDY_PRESENCE(pre));
3542 create_sendto_item(menu, sg, &group, buddy,
3543 purple_buddy_get_account(buddy), purple_buddy_get_name(buddy), e2ee_enabled);
3546 g_list_free(list);
3547 g_slist_free(buds);
3551 g_object_unref(sg);
3553 gtk_widget_show(win->menu->send_to);
3554 /* TODO: This should never be insensitive. Possibly hidden or not. */
3555 if (!group)
3556 gtk_widget_set_sensitive(win->menu->send_to, FALSE);
3557 update_send_to_selection(win);
3560 PurpleImage *
3561 _pidgin_e2ee_stock_icon_get(const gchar *stock_name)
3563 gchar filename[100], *path;
3564 PurpleImage *image;
3566 /* core is quitting */
3567 if (e2ee_stock == NULL)
3568 return NULL;
3570 if (g_hash_table_lookup_extended(e2ee_stock, stock_name, NULL, (gpointer*)&image))
3571 return image;
3573 g_snprintf(filename, sizeof(filename), "e2ee-%s.png", stock_name);
3574 path = g_build_filename(PURPLE_DATADIR, "pidgin", "icons",
3575 "hicolor", "16x16", "status", filename, NULL);
3576 image = purple_image_new_from_file(path, NULL);
3577 g_free(path);
3579 g_hash_table_insert(e2ee_stock, g_strdup(stock_name), image);
3580 return image;
3583 static void
3584 generate_e2ee_controls(PidginConvWindow *win)
3586 PidginConversation *gtkconv;
3587 PurpleConversation *conv;
3588 PurpleE2eeState *state;
3589 PurpleE2eeProvider *provider;
3590 GtkWidget *menu;
3591 GList *menu_actions, *it;
3592 GtkWidget *e2ee_image;
3594 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
3595 g_return_if_fail(gtkconv != NULL);
3597 conv = gtkconv->active_conv;
3598 g_return_if_fail(conv != NULL);
3600 if (win->menu->e2ee != NULL) {
3601 gtk_widget_destroy(win->menu->e2ee);
3602 win->menu->e2ee = NULL;
3605 provider = purple_e2ee_provider_get_main();
3606 state = purple_conversation_get_e2ee_state(conv);
3607 if (state == NULL || provider == NULL)
3608 return;
3609 if (purple_e2ee_state_get_provider(state) != provider)
3610 return;
3612 win->menu->e2ee = gtk_image_menu_item_new_with_label(
3613 purple_e2ee_provider_get_name(provider));
3615 menu = gtk_menu_new();
3616 gtk_menu_shell_insert(GTK_MENU_SHELL(win->menu->menubar),
3617 win->menu->e2ee, 3);
3618 gtk_menu_item_set_submenu(GTK_MENU_ITEM(win->menu->e2ee), menu);
3620 e2ee_image = e2ee_state_to_gtkimage(state);
3621 if (e2ee_image) {
3622 gtk_image_menu_item_set_image(
3623 GTK_IMAGE_MENU_ITEM(win->menu->e2ee), e2ee_image);
3626 gtk_widget_set_tooltip_text(win->menu->e2ee,
3627 purple_e2ee_state_get_name(state));
3629 menu_actions = purple_e2ee_provider_get_conv_menu_actions(provider, conv);
3630 for (it = menu_actions; it; it = g_list_next(it)) {
3631 PurpleActionMenu *action = it->data;
3633 gtk_widget_show_all(
3634 pidgin_append_menu_action(menu, action, conv));
3636 g_list_free(menu_actions);
3638 gtk_widget_show(win->menu->e2ee);
3639 gtk_widget_show(menu);
3642 static const char *
3643 get_chat_user_status_icon(PurpleChatConversation *chat, const char *name, PurpleChatUserFlags flags)
3645 const char *image = NULL;
3647 if (flags & PURPLE_CHAT_USER_FOUNDER) {
3648 image = PIDGIN_STOCK_STATUS_FOUNDER;
3649 } else if (flags & PURPLE_CHAT_USER_OP) {
3650 image = PIDGIN_STOCK_STATUS_OPERATOR;
3651 } else if (flags & PURPLE_CHAT_USER_HALFOP) {
3652 image = PIDGIN_STOCK_STATUS_HALFOP;
3653 } else if (flags & PURPLE_CHAT_USER_VOICE) {
3654 image = PIDGIN_STOCK_STATUS_VOICE;
3655 } else if ((!flags) && purple_chat_conversation_is_ignored_user(chat, name)) {
3656 image = PIDGIN_STOCK_STATUS_IGNORED;
3657 } else {
3658 return NULL;
3660 return image;
3663 static void
3664 deleting_chat_user_cb(PurpleChatUser *cb)
3666 GtkTreeRowReference *ref = purple_chat_user_get_ui_data(cb);
3668 if (ref) {
3669 gtk_tree_row_reference_free(ref);
3670 purple_chat_user_set_ui_data(cb, NULL);
3674 static void
3675 add_chat_user_common(PurpleChatConversation *chat, PurpleChatUser *cb, const char *old_name)
3677 PidginConversation *gtkconv;
3678 PurpleConversation *conv;
3679 PidginChatPane *gtkchat;
3680 PurpleConnection *gc;
3681 PurpleProtocol *protocol;
3682 GtkTreeModel *tm;
3683 GtkListStore *ls;
3684 GtkTreePath *newpath;
3685 const char *stock;
3686 GtkTreeIter iter;
3687 gboolean is_me = FALSE;
3688 gboolean is_buddy;
3689 const gchar *name, *alias;
3690 gchar *tmp, *alias_key;
3691 PurpleChatUserFlags flags;
3692 GdkRGBA *color = NULL;
3694 alias = purple_chat_user_get_alias(cb);
3695 name = purple_chat_user_get_name(cb);
3696 flags = purple_chat_user_get_flags(cb);
3698 conv = PURPLE_CONVERSATION(chat);
3699 gtkconv = PIDGIN_CONVERSATION(conv);
3700 gtkchat = gtkconv->u.chat;
3701 gc = purple_conversation_get_connection(conv);
3703 if (!gc || !(protocol = purple_connection_get_protocol(gc)))
3704 return;
3706 tm = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
3707 ls = GTK_LIST_STORE(tm);
3709 stock = get_chat_user_status_icon(chat, name, flags);
3711 if (purple_strequal(purple_chat_conversation_get_nick(chat), purple_normalize(purple_conversation_get_account(conv), old_name != NULL ? old_name : name)))
3712 is_me = TRUE;
3714 is_buddy = purple_chat_user_is_buddy(cb);
3716 tmp = g_utf8_casefold(alias, -1);
3717 alias_key = g_utf8_collate_key(tmp, -1);
3718 g_free(tmp);
3720 if (is_me) {
3721 #if 0
3722 /* TODO WEBKIT: No tags in webkit stuff, yet. */
3723 GtkTextTag *tag = gtk_text_tag_table_lookup(
3724 gtk_text_buffer_get_tag_table(GTK_IMHTML(gtkconv->webview)->text_buffer),
3725 "send-name");
3726 g_object_get(tag, "foreground-rgba", &color, NULL);
3727 #endif /* if 0 */
3728 } else {
3729 GtkTextTag *tag;
3730 if ((tag = get_buddy_tag(chat, name, 0, FALSE)))
3731 g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_NORMAL, NULL);
3732 if ((tag = get_buddy_tag(chat, name, PURPLE_MESSAGE_NICK, FALSE)))
3733 g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_NORMAL, NULL);
3734 color = (GdkRGBA*)get_nick_color(gtkconv, name);
3737 gtk_list_store_insert_with_values(ls, &iter,
3739 * The GTK docs are mute about the effects of the "row" value for performance.
3740 * X-Chat hardcodes their value to 0 (prepend) and -1 (append), so we will too.
3741 * It *might* be faster to search the gtk_list_store and set row accurately,
3742 * but no one in #gtk+ seems to know anything about it either.
3743 * Inserting in the "wrong" location has no visible ill effects. - F.P.
3745 -1, /* "row" */
3746 CHAT_USERS_ICON_STOCK_COLUMN, stock,
3747 CHAT_USERS_ALIAS_COLUMN, alias,
3748 CHAT_USERS_ALIAS_KEY_COLUMN, alias_key,
3749 CHAT_USERS_NAME_COLUMN, name,
3750 CHAT_USERS_FLAGS_COLUMN, flags,
3751 CHAT_USERS_COLOR_COLUMN, color,
3752 CHAT_USERS_WEIGHT_COLUMN, is_buddy ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL,
3753 -1);
3755 if (purple_chat_user_get_ui_data(cb)) {
3756 GtkTreeRowReference *ref = purple_chat_user_get_ui_data(cb);
3757 gtk_tree_row_reference_free(ref);
3760 newpath = gtk_tree_model_get_path(tm, &iter);
3761 purple_chat_user_set_ui_data(cb, gtk_tree_row_reference_new(tm, newpath));
3762 gtk_tree_path_free(newpath);
3764 #if 0
3765 if (is_me && color)
3766 gdk_rgba_free(color);
3767 #endif
3768 g_free(alias_key);
3771 static void topic_callback(GtkWidget *w, PidginConversation *gtkconv)
3773 PurpleProtocol *protocol = NULL;
3774 PurpleConnection *gc;
3775 PurpleConversation *conv = gtkconv->active_conv;
3776 PidginChatPane *gtkchat;
3777 char *new_topic;
3778 const char *current_topic;
3780 gc = purple_conversation_get_connection(conv);
3782 if(!gc || !(protocol = purple_connection_get_protocol(gc)))
3783 return;
3785 if(!PURPLE_PROTOCOL_IMPLEMENTS(protocol, CHAT, set_topic))
3786 return;
3788 gtkconv = PIDGIN_CONVERSATION(conv);
3789 gtkchat = gtkconv->u.chat;
3790 new_topic = g_strdup(gtk_entry_get_text(GTK_ENTRY(gtkchat->topic_text)));
3791 current_topic = purple_chat_conversation_get_topic(PURPLE_CHAT_CONVERSATION(conv));
3793 if(current_topic && !g_utf8_collate(new_topic, current_topic)){
3794 g_free(new_topic);
3795 return;
3798 if (current_topic)
3799 gtk_entry_set_text(GTK_ENTRY(gtkchat->topic_text), current_topic);
3800 else
3801 gtk_entry_set_text(GTK_ENTRY(gtkchat->topic_text), "");
3803 purple_protocol_chat_iface_set_topic(protocol, gc, purple_chat_conversation_get_id(PURPLE_CHAT_CONVERSATION(conv)),
3804 new_topic);
3806 g_free(new_topic);
3809 static gint
3810 sort_chat_users(GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, gpointer userdata)
3812 PurpleChatUserFlags f1 = 0, f2 = 0;
3813 char *user1 = NULL, *user2 = NULL;
3814 gboolean buddy1 = FALSE, buddy2 = FALSE;
3815 gint ret = 0;
3817 gtk_tree_model_get(model, a,
3818 CHAT_USERS_ALIAS_KEY_COLUMN, &user1,
3819 CHAT_USERS_FLAGS_COLUMN, &f1,
3820 CHAT_USERS_WEIGHT_COLUMN, &buddy1,
3821 -1);
3822 gtk_tree_model_get(model, b,
3823 CHAT_USERS_ALIAS_KEY_COLUMN, &user2,
3824 CHAT_USERS_FLAGS_COLUMN, &f2,
3825 CHAT_USERS_WEIGHT_COLUMN, &buddy2,
3826 -1);
3828 /* Only sort by membership levels */
3829 f1 &= PURPLE_CHAT_USER_VOICE | PURPLE_CHAT_USER_HALFOP | PURPLE_CHAT_USER_OP |
3830 PURPLE_CHAT_USER_FOUNDER;
3831 f2 &= PURPLE_CHAT_USER_VOICE | PURPLE_CHAT_USER_HALFOP | PURPLE_CHAT_USER_OP |
3832 PURPLE_CHAT_USER_FOUNDER;
3834 ret = g_strcmp0(user1, user2);
3836 if (user1 != NULL && user2 != NULL) {
3837 if (f1 != f2) {
3838 /* sort more important users first */
3839 ret = (f1 > f2) ? -1 : 1;
3840 } else if (buddy1 != buddy2) {
3841 ret = (buddy1 > buddy2) ? -1 : 1;
3845 g_free(user1);
3846 g_free(user2);
3848 return ret;
3851 static void
3852 update_chat_alias(PurpleBuddy *buddy, PurpleChatConversation *chat, PurpleConnection *gc, PurpleProtocol *protocol)
3854 PidginConversation *gtkconv = PIDGIN_CONVERSATION(PURPLE_CONVERSATION(chat));
3855 PurpleAccount *account = purple_conversation_get_account(PURPLE_CONVERSATION(chat));
3856 GtkTreeModel *model;
3857 char *normalized_name;
3858 GtkTreeIter iter;
3859 int f;
3861 g_return_if_fail(buddy != NULL);
3862 g_return_if_fail(chat != NULL);
3864 /* This is safe because this callback is only used in chats, not IMs. */
3865 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkconv->u.chat->list));
3867 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &iter))
3868 return;
3870 normalized_name = g_strdup(purple_normalize(account, purple_buddy_get_name(buddy)));
3872 do {
3873 char *name;
3875 gtk_tree_model_get(model, &iter, CHAT_USERS_NAME_COLUMN, &name, -1);
3877 if (purple_strequal(normalized_name, purple_normalize(account, name))) {
3878 const char *alias = name;
3879 char *tmp;
3880 char *alias_key = NULL;
3881 PurpleBuddy *buddy2;
3883 if (!purple_strequal(purple_chat_conversation_get_nick(chat), purple_normalize(account, name))) {
3884 /* This user is not me, so look into updating the alias. */
3886 if ((buddy2 = purple_blist_find_buddy(account, name)) != NULL) {
3887 alias = purple_buddy_get_contact_alias(buddy2);
3890 tmp = g_utf8_casefold(alias, -1);
3891 alias_key = g_utf8_collate_key(tmp, -1);
3892 g_free(tmp);
3894 gtk_list_store_set(GTK_LIST_STORE(model), &iter,
3895 CHAT_USERS_ALIAS_COLUMN, alias,
3896 CHAT_USERS_ALIAS_KEY_COLUMN, alias_key,
3897 -1);
3898 g_free(alias_key);
3900 g_free(name);
3901 break;
3904 f = gtk_tree_model_iter_next(model, &iter);
3906 g_free(name);
3907 } while (f != 0);
3909 g_free(normalized_name);
3912 static void
3913 blist_node_aliased_cb(PurpleBlistNode *node, const char *old_alias, PurpleChatConversation *chat)
3915 PurpleConnection *gc;
3916 PurpleProtocol *protocol;
3917 PurpleConversation *conv = PURPLE_CONVERSATION(chat);
3919 g_return_if_fail(node != NULL);
3920 g_return_if_fail(conv != NULL);
3922 gc = purple_conversation_get_connection(conv);
3923 g_return_if_fail(gc != NULL);
3924 g_return_if_fail(purple_connection_get_protocol(gc) != NULL);
3925 protocol = purple_connection_get_protocol(gc);
3927 if (purple_protocol_get_options(protocol) & OPT_PROTO_UNIQUE_CHATNAME)
3928 return;
3930 if (PURPLE_IS_CONTACT(node))
3932 PurpleBlistNode *bnode;
3934 for(bnode = node->child; bnode; bnode = bnode->next) {
3936 if(!PURPLE_IS_BUDDY(bnode))
3937 continue;
3939 update_chat_alias((PurpleBuddy *)bnode, chat, gc, protocol);
3942 else if (PURPLE_IS_BUDDY(node))
3943 update_chat_alias((PurpleBuddy *)node, chat, gc, protocol);
3944 else if (PURPLE_IS_CHAT(node) &&
3945 purple_conversation_get_account(conv) == purple_chat_get_account((PurpleChat*)node))
3947 if (old_alias == NULL || g_utf8_collate(old_alias, purple_conversation_get_title(conv)) == 0)
3948 pidgin_conv_update_fields(conv, PIDGIN_CONV_SET_TITLE);
3952 static void
3953 buddy_cb_common(PurpleBuddy *buddy, PurpleChatConversation *chat, gboolean is_buddy)
3955 GtkTreeModel *model;
3956 char *normalized_name;
3957 GtkTreeIter iter;
3958 GtkTextTag *texttag;
3959 PurpleConversation *conv = PURPLE_CONVERSATION(chat);
3960 int f;
3962 g_return_if_fail(buddy != NULL);
3963 g_return_if_fail(conv != NULL);
3965 /* Do nothing if the buddy does not belong to the conv's account */
3966 if (purple_buddy_get_account(buddy) != purple_conversation_get_account(conv))
3967 return;
3969 /* This is safe because this callback is only used in chats, not IMs. */
3970 model = gtk_tree_view_get_model(GTK_TREE_VIEW(PIDGIN_CONVERSATION(conv)->u.chat->list));
3972 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &iter))
3973 return;
3975 normalized_name = g_strdup(purple_normalize(purple_conversation_get_account(conv), purple_buddy_get_name(buddy)));
3977 do {
3978 char *name;
3980 gtk_tree_model_get(model, &iter, CHAT_USERS_NAME_COLUMN, &name, -1);
3982 if (purple_strequal(normalized_name, purple_normalize(purple_conversation_get_account(conv), name))) {
3983 gtk_list_store_set(GTK_LIST_STORE(model), &iter,
3984 CHAT_USERS_WEIGHT_COLUMN, is_buddy ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL, -1);
3985 g_free(name);
3986 break;
3989 f = gtk_tree_model_iter_next(model, &iter);
3991 g_free(name);
3992 } while (f != 0);
3994 g_free(normalized_name);
3996 blist_node_aliased_cb((PurpleBlistNode *)buddy, NULL, chat);
3998 texttag = get_buddy_tag(chat, purple_buddy_get_name(buddy), 0, FALSE); /* XXX: do we want the normalized name? */
3999 if (texttag) {
4000 g_object_set(texttag, "weight", is_buddy ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL, NULL);
4004 static void
4005 buddy_added_cb(PurpleBlistNode *node, PurpleChatConversation *chat)
4007 if (!PURPLE_IS_BUDDY(node))
4008 return;
4010 buddy_cb_common(PURPLE_BUDDY(node), chat, TRUE);
4013 static void
4014 buddy_removed_cb(PurpleBlistNode *node, PurpleChatConversation *chat)
4016 if (!PURPLE_IS_BUDDY(node))
4017 return;
4019 /* If there's another buddy for the same "dude" on the list, do nothing. */
4020 if (purple_blist_find_buddy(purple_buddy_get_account(PURPLE_BUDDY(node)),
4021 purple_buddy_get_name(PURPLE_BUDDY(node))) != NULL)
4022 return;
4024 buddy_cb_common(PURPLE_BUDDY(node), chat, FALSE);
4027 static void
4028 minimum_entry_lines_pref_cb(const char *name,
4029 PurplePrefType type,
4030 gconstpointer value,
4031 gpointer data)
4033 GList *l = purple_conversations_get_all();
4034 PurpleConversation *conv;
4035 while (l != NULL)
4037 #if 0
4038 conv = (PurpleConversation *)l->data;
4040 if (PIDGIN_IS_PIDGIN_CONVERSATION(conv))
4041 resize_webview_cb(PIDGIN_CONVERSATION(conv));
4042 #endif
4043 l = l->next;
4047 static void
4048 setup_chat_topic(PidginConversation *gtkconv, GtkWidget *vbox)
4050 PurpleConversation *conv = gtkconv->active_conv;
4051 PurpleConnection *gc = purple_conversation_get_connection(conv);
4052 PurpleProtocol *protocol = purple_connection_get_protocol(gc);
4053 if (purple_protocol_get_options(protocol) & OPT_PROTO_CHAT_TOPIC)
4055 GtkWidget *hbox, *label;
4056 PidginChatPane *gtkchat = gtkconv->u.chat;
4058 hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, PIDGIN_HIG_BOX_SPACE);
4059 gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0);
4061 label = gtk_label_new(_("Topic:"));
4062 gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 0);
4064 gtkchat->topic_text = gtk_entry_new();
4065 gtk_widget_set_size_request(gtkchat->topic_text, -1, BUDDYICON_SIZE_MIN);
4067 if(!PURPLE_PROTOCOL_IMPLEMENTS(protocol, CHAT, set_topic)) {
4068 gtk_editable_set_editable(GTK_EDITABLE(gtkchat->topic_text), FALSE);
4069 } else {
4070 g_signal_connect(G_OBJECT(gtkchat->topic_text), "activate",
4071 G_CALLBACK(topic_callback), gtkconv);
4074 gtk_box_pack_start(GTK_BOX(hbox), gtkchat->topic_text, TRUE, TRUE, 0);
4075 g_signal_connect(G_OBJECT(gtkchat->topic_text), "key_press_event",
4076 G_CALLBACK(entry_key_press_cb), gtkconv);
4080 static gboolean
4081 pidgin_conv_userlist_create_tooltip(GtkWidget *tipwindow, GtkTreePath *path,
4082 gpointer userdata, int *w, int *h)
4084 PidginConversation *gtkconv = userdata;
4085 GtkTreeIter iter;
4086 GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkconv->u.chat->list));
4087 PurpleConversation *conv = gtkconv->active_conv;
4088 PurpleBlistNode *node;
4089 PurpleProtocol *protocol;
4090 PurpleAccount *account = purple_conversation_get_account(conv);
4091 char *who = NULL;
4093 if (purple_account_get_connection(account) == NULL)
4094 return FALSE;
4096 if (!gtk_tree_model_get_iter(GTK_TREE_MODEL(model), &iter, path))
4097 return FALSE;
4099 gtk_tree_model_get(GTK_TREE_MODEL(model), &iter, CHAT_USERS_NAME_COLUMN, &who, -1);
4101 protocol = purple_connection_get_protocol(purple_account_get_connection(account));
4102 node = (PurpleBlistNode*)(purple_blist_find_buddy(purple_conversation_get_account(conv), who));
4103 if (node && protocol && (purple_protocol_get_options(protocol) & OPT_PROTO_UNIQUE_CHATNAME))
4104 pidgin_blist_draw_tooltip(node, gtkconv->infopane);
4106 g_free(who);
4107 return FALSE;
4110 static void
4111 setup_chat_userlist(PidginConversation *gtkconv, GtkWidget *hpaned)
4113 PidginChatPane *gtkchat = gtkconv->u.chat;
4114 GtkWidget *lbox, *list;
4115 GtkListStore *ls;
4116 GtkCellRenderer *rend;
4117 GtkTreeViewColumn *col;
4118 int ul_width;
4119 void *blist_handle = purple_blist_get_handle();
4120 PurpleConversation *conv = gtkconv->active_conv;
4122 /* Build the right pane. */
4123 lbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, PIDGIN_HIG_BOX_SPACE);
4124 gtk_paned_pack2(GTK_PANED(hpaned), lbox, FALSE, TRUE);
4125 gtk_widget_show(lbox);
4127 /* Setup the label telling how many people are in the room. */
4128 gtkchat->count = gtk_label_new(_("0 people in room"));
4129 gtk_label_set_ellipsize(GTK_LABEL(gtkchat->count), PANGO_ELLIPSIZE_END);
4130 gtk_box_pack_start(GTK_BOX(lbox), gtkchat->count, FALSE, FALSE, 0);
4131 gtk_widget_show(gtkchat->count);
4133 /* Setup the list of users. */
4135 ls = gtk_list_store_new(CHAT_USERS_COLUMNS, GDK_TYPE_PIXBUF, G_TYPE_STRING,
4136 G_TYPE_STRING, G_TYPE_STRING, G_TYPE_INT,
4137 GDK_TYPE_RGBA, G_TYPE_INT, G_TYPE_STRING);
4138 gtk_tree_sortable_set_sort_func(GTK_TREE_SORTABLE(ls), CHAT_USERS_ALIAS_KEY_COLUMN,
4139 sort_chat_users, NULL, NULL);
4141 list = gtk_tree_view_new_with_model(GTK_TREE_MODEL(ls));
4143 /* Allow a user to specify gtkrc settings for the chat userlist only */
4144 gtk_widget_set_name(list, "pidgin_conv_userlist");
4146 rend = gtk_cell_renderer_pixbuf_new();
4147 g_object_set(G_OBJECT(rend),
4148 "stock-size", gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_EXTRA_SMALL),
4149 NULL);
4150 col = gtk_tree_view_column_new_with_attributes(NULL, rend,
4151 "stock-id", CHAT_USERS_ICON_STOCK_COLUMN, NULL);
4152 gtk_tree_view_column_set_sizing(col, GTK_TREE_VIEW_COLUMN_AUTOSIZE);
4153 gtk_tree_view_append_column(GTK_TREE_VIEW(list), col);
4154 ul_width = purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/chat/userlist_width");
4155 gtk_widget_set_size_request(lbox, ul_width, -1);
4157 /* Hack to prevent completely collapsed userlist coming back with a 1 pixel width.
4158 * I would have liked to use the GtkPaned "max-position", but for some reason that didn't work */
4159 if (ul_width == 0)
4160 gtk_paned_set_position(GTK_PANED(hpaned), 999999);
4162 g_signal_connect(G_OBJECT(list), "button_press_event",
4163 G_CALLBACK(right_click_chat_cb), gtkconv);
4164 g_signal_connect(G_OBJECT(list), "row-activated",
4165 G_CALLBACK(activate_list_cb), gtkconv);
4166 g_signal_connect(G_OBJECT(list), "popup-menu",
4167 G_CALLBACK(gtkconv_chat_popup_menu_cb), gtkconv);
4168 g_signal_connect(G_OBJECT(lbox), "size-allocate", G_CALLBACK(lbox_size_allocate_cb), gtkconv);
4170 pidgin_tooltip_setup_for_treeview(list, gtkconv,
4171 pidgin_conv_userlist_create_tooltip, NULL);
4173 rend = gtk_cell_renderer_text_new();
4174 g_object_set(rend,
4175 "foreground-set", TRUE,
4176 "weight-set", TRUE,
4177 NULL);
4178 g_object_set(G_OBJECT(rend), "editable", TRUE, NULL);
4180 col = gtk_tree_view_column_new_with_attributes(NULL, rend,
4181 "text", CHAT_USERS_ALIAS_COLUMN,
4182 "foreground-rgba", CHAT_USERS_COLOR_COLUMN,
4183 "weight", CHAT_USERS_WEIGHT_COLUMN,
4184 NULL);
4186 purple_signal_connect(blist_handle, "blist-node-added",
4187 gtkchat, PURPLE_CALLBACK(buddy_added_cb), conv);
4188 purple_signal_connect(blist_handle, "blist-node-removed",
4189 gtkchat, PURPLE_CALLBACK(buddy_removed_cb), conv);
4190 purple_signal_connect(blist_handle, "blist-node-aliased",
4191 gtkchat, PURPLE_CALLBACK(blist_node_aliased_cb), conv);
4193 gtk_tree_view_column_set_expand(col, TRUE);
4194 g_object_set(rend, "ellipsize", PANGO_ELLIPSIZE_END, NULL);
4196 gtk_tree_view_append_column(GTK_TREE_VIEW(list), col);
4198 gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(list), FALSE);
4199 gtk_widget_show(list);
4201 gtkchat->list = list;
4203 gtk_box_pack_start(GTK_BOX(lbox),
4204 pidgin_make_scrollable(list, GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC, GTK_SHADOW_IN, -1, -1),
4205 TRUE, TRUE, 0);
4208 static gboolean
4209 pidgin_conv_create_tooltip(GtkWidget *tipwindow, gpointer userdata, int *w, int *h)
4211 PurpleBlistNode *node = NULL;
4212 PurpleConversation *conv;
4213 PidginConversation *gtkconv = userdata;
4215 conv = gtkconv->active_conv;
4216 if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
4217 node = (PurpleBlistNode*)(purple_blist_find_chat(purple_conversation_get_account(conv), purple_conversation_get_name(conv)));
4218 if (!node)
4219 node = g_object_get_data(G_OBJECT(gtkconv->history), "transient_chat");
4220 } else {
4221 node = (PurpleBlistNode*)(purple_blist_find_buddy(purple_conversation_get_account(conv), purple_conversation_get_name(conv)));
4222 #if 0
4223 /* Using the transient blist nodes to show the tooltip doesn't quite work yet. */
4224 if (!node)
4225 node = g_object_get_data(G_OBJECT(gtkconv->webview), "transient_buddy");
4226 #endif
4229 if (node)
4230 pidgin_blist_draw_tooltip(node, gtkconv->infopane);
4231 return FALSE;
4234 static GtkWidget *
4235 setup_common_pane(PidginConversation *gtkconv)
4237 GtkWidget *vbox, *sw, *event_box, *view;
4238 GtkCellRenderer *rend;
4239 GtkTreePath *path;
4240 PurpleConversation *conv = gtkconv->active_conv;
4241 PurpleBuddy *buddy;
4242 gboolean chat = PURPLE_IS_CHAT_CONVERSATION(conv);
4243 int buddyicon_size = 0;
4245 /* Setup the top part of the pane */
4246 vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, PIDGIN_HIG_BOX_SPACE);
4247 gtk_widget_show(vbox);
4249 /* Setup the info pane */
4250 event_box = gtk_event_box_new();
4251 gtk_event_box_set_visible_window(GTK_EVENT_BOX(event_box), FALSE);
4252 gtk_widget_show(event_box);
4253 gtkconv->infopane_hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
4254 gtk_box_pack_start(GTK_BOX(vbox), event_box, FALSE, FALSE, 0);
4255 gtk_container_add(GTK_CONTAINER(event_box), gtkconv->infopane_hbox);
4256 gtk_widget_show(gtkconv->infopane_hbox);
4257 gtk_widget_add_events(event_box,
4258 GDK_POINTER_MOTION_MASK | GDK_LEAVE_NOTIFY_MASK);
4259 g_signal_connect(G_OBJECT(event_box), "button-press-event",
4260 G_CALLBACK(infopane_press_cb), gtkconv);
4262 pidgin_tooltip_setup_for_widget(event_box, gtkconv,
4263 pidgin_conv_create_tooltip, NULL);
4265 gtkconv->infopane = gtk_cell_view_new();
4266 gtkconv->infopane_model = gtk_list_store_new(CONV_NUM_COLUMNS, G_TYPE_STRING, G_TYPE_STRING, GDK_TYPE_PIXBUF, GDK_TYPE_PIXBUF);
4267 gtk_cell_view_set_model(GTK_CELL_VIEW(gtkconv->infopane),
4268 GTK_TREE_MODEL(gtkconv->infopane_model));
4269 g_object_unref(gtkconv->infopane_model);
4270 gtk_list_store_append(gtkconv->infopane_model, &(gtkconv->infopane_iter));
4271 gtk_box_pack_start(GTK_BOX(gtkconv->infopane_hbox), gtkconv->infopane, TRUE, TRUE, 0);
4272 path = gtk_tree_path_new_from_string("0");
4273 gtk_cell_view_set_displayed_row(GTK_CELL_VIEW(gtkconv->infopane), path);
4274 gtk_tree_path_free(path);
4276 if (chat) {
4277 /* This empty widget is used to ensure that the infopane is consistently
4278 sized for chat windows. The correct fix is to put an icon in the chat
4279 window as well, because that would make "Set Custom Icon" consistent
4280 for both the buddy list and the chat window, but PidginConversation
4281 is pretty much stuck until 3.0. */
4282 GtkWidget *sizing_vbox;
4283 sizing_vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
4284 gtk_widget_set_size_request(sizing_vbox, -1, BUDDYICON_SIZE_MIN);
4285 gtk_box_pack_start(GTK_BOX(gtkconv->infopane_hbox), sizing_vbox, FALSE, FALSE, 0);
4286 gtk_widget_show(sizing_vbox);
4288 else {
4289 gtkconv->u.im->icon_container = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
4291 if ((buddy = purple_blist_find_buddy(purple_conversation_get_account(conv),
4292 purple_conversation_get_name(conv))) != NULL) {
4293 PurpleContact *contact = purple_buddy_get_contact(buddy);
4294 if (contact) {
4295 buddyicon_size = purple_blist_node_get_int((PurpleBlistNode*)contact, "pidgin-infopane-iconsize");
4298 buddyicon_size = CLAMP(buddyicon_size, BUDDYICON_SIZE_MIN, BUDDYICON_SIZE_MAX);
4299 gtk_widget_set_size_request(gtkconv->u.im->icon_container, -1, buddyicon_size);
4301 gtk_box_pack_start(GTK_BOX(gtkconv->infopane_hbox),
4302 gtkconv->u.im->icon_container, FALSE, FALSE, 0);
4304 gtk_widget_show(gtkconv->u.im->icon_container);
4307 gtk_widget_show(gtkconv->infopane);
4309 rend = gtk_cell_renderer_pixbuf_new();
4310 gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(gtkconv->infopane), rend, FALSE);
4311 gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(gtkconv->infopane), rend, "stock-id", CONV_ICON_COLUMN, NULL);
4312 g_object_set(rend, "xalign", 0.0, "xpad", 6, "ypad", 0,
4313 "stock-size", gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_EXTRA_SMALL),
4314 NULL);
4316 rend = gtk_cell_renderer_text_new();
4317 gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(gtkconv->infopane), rend, TRUE);
4318 gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(gtkconv->infopane), rend, "markup", CONV_TEXT_COLUMN, NULL);
4319 g_object_set(rend, "ypad", 0, "yalign", 0.5, NULL);
4321 g_object_set(rend, "ellipsize", PANGO_ELLIPSIZE_END, NULL);
4323 rend = gtk_cell_renderer_pixbuf_new();
4324 gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(gtkconv->infopane), rend, FALSE);
4325 gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(gtkconv->infopane), rend, "pixbuf", CONV_PROTOCOL_ICON_COLUMN, NULL);
4326 g_object_set(rend, "xalign", 0.0, "xpad", 3, "ypad", 0, NULL);
4328 rend = gtk_cell_renderer_pixbuf_new();
4329 gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(gtkconv->infopane), rend, FALSE);
4330 gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(gtkconv->infopane), rend, "pixbuf", CONV_EMBLEM_COLUMN, NULL);
4331 g_object_set(rend, "xalign", 0.0, "xpad", 6, "ypad", 0, NULL);
4333 /* Setup the history widget */
4334 sw = gtk_scrolled_window_new(NULL, NULL);
4335 gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(sw), GTK_SHADOW_IN);
4336 gtk_scrolled_window_set_policy(
4337 GTK_SCROLLED_WINDOW(sw),
4338 GTK_POLICY_NEVER,
4339 GTK_POLICY_ALWAYS
4342 gtkconv->history_buffer = talkatu_history_buffer_new();
4343 gtkconv->history = talkatu_history_new();
4344 gtk_text_view_set_buffer(GTK_TEXT_VIEW(gtkconv->history), gtkconv->history_buffer);
4345 gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(gtkconv->history), GTK_WRAP_WORD);
4346 gtk_container_add(GTK_CONTAINER(sw), gtkconv->history);
4348 if (chat) {
4349 GtkWidget *hpaned;
4351 /* Add the topic */
4352 setup_chat_topic(gtkconv, vbox);
4354 /* Add the talkatu history */
4355 hpaned = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL);
4356 gtk_box_pack_start(GTK_BOX(vbox), hpaned, TRUE, TRUE, 0);
4357 gtk_widget_show(hpaned);
4358 gtk_paned_pack1(GTK_PANED(hpaned), sw, TRUE, TRUE);
4360 /* Now add the userlist */
4361 setup_chat_userlist(gtkconv, hpaned);
4362 } else {
4363 gtk_box_pack_start(GTK_BOX(vbox), sw, TRUE, TRUE, 0);
4365 gtk_widget_show_all(sw);
4367 g_object_set_data(G_OBJECT(gtkconv->history), "gtkconv", gtkconv);
4369 g_signal_connect(G_OBJECT(gtkconv->history), "key_press_event",
4370 G_CALLBACK(refocus_entry_cb), gtkconv);
4371 g_signal_connect(G_OBJECT(gtkconv->history), "key_release_event",
4372 G_CALLBACK(refocus_entry_cb), gtkconv);
4374 gtkconv->lower_hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, PIDGIN_HIG_BOX_SPACE);
4375 gtk_box_pack_start(GTK_BOX(vbox), gtkconv->lower_hbox, FALSE, FALSE, 0);
4376 gtk_widget_show(gtkconv->lower_hbox);
4378 /* Setup the entry widget and all signals */
4379 gtkconv->editor = talkatu_editor_new();
4380 talkatu_editor_set_buffer(TALKATU_EDITOR(gtkconv->editor), talkatu_html_buffer_new());
4381 gtk_box_pack_start(GTK_BOX(gtkconv->lower_hbox), gtkconv->editor, TRUE, TRUE, 0);
4383 view = talkatu_editor_get_view(TALKATU_EDITOR(gtkconv->editor));
4384 gtk_widget_set_name(view, "pidgin_conv_entry");
4385 talkatu_view_set_send_binding(TALKATU_VIEW(view), TALKATU_VIEW_SEND_BINDING_RETURN | TALKATU_VIEW_SEND_BINDING_KP_ENTER);
4386 g_signal_connect(
4387 G_OBJECT(view),
4388 "send-message",
4389 G_CALLBACK(send_cb),
4390 gtkconv
4393 if (!chat) {
4394 /* For sending typing notifications for IMs */
4395 gtkconv->u.im->typing_timer = 0;
4396 gtkconv->u.im->animate = purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/im/animate_buddy_icons");
4397 gtkconv->u.im->show_icon = TRUE;
4400 return vbox;
4403 static PidginConversation *
4404 pidgin_conv_find_gtkconv(PurpleConversation * conv)
4406 PurpleBuddy *bud = purple_blist_find_buddy(purple_conversation_get_account(conv), purple_conversation_get_name(conv));
4407 PurpleContact *c;
4408 PurpleBlistNode *cn, *bn;
4410 if (!bud)
4411 return NULL;
4413 if (!(c = purple_buddy_get_contact(bud)))
4414 return NULL;
4416 cn = PURPLE_BLIST_NODE(c);
4417 for (bn = purple_blist_node_get_first_child(cn); bn; bn = purple_blist_node_get_sibling_next(bn)) {
4418 PurpleBuddy *b = PURPLE_BUDDY(bn);
4419 PurpleIMConversation *im;
4420 if ((im = purple_conversations_find_im_with_account(purple_buddy_get_name(b), purple_buddy_get_account(b)))) {
4421 if (PIDGIN_CONVERSATION(PURPLE_CONVERSATION(im)))
4422 return PIDGIN_CONVERSATION(PURPLE_CONVERSATION(im));
4426 return NULL;
4429 static void
4430 buddy_update_cb(PurpleBlistNode *bnode, gpointer null)
4432 GList *list;
4434 g_return_if_fail(bnode);
4435 if (!PURPLE_IS_BUDDY(bnode))
4436 return;
4438 for (list = pidgin_conv_windows_get_list(); list; list = list->next)
4440 PidginConvWindow *win = list->data;
4441 PurpleConversation *conv = pidgin_conv_window_get_active_conversation(win);
4443 if (!PURPLE_IS_IM_CONVERSATION(conv))
4444 continue;
4446 pidgin_conv_update_fields(conv, PIDGIN_CONV_MENU);
4450 static gboolean
4451 ignore_middle_click(GtkWidget *widget, GdkEventButton *e, gpointer null)
4453 /* A click on the pane is propagated to the notebook containing the pane.
4454 * So if Stu accidentally aims high and middle clicks on the pane-handle,
4455 * it causes a conversation tab to close. Let's stop that from happening.
4457 if (e->button == GDK_BUTTON_MIDDLE && e->type == GDK_BUTTON_PRESS)
4458 return TRUE;
4459 return FALSE;
4462 /**************************************************************************
4463 * Conversation UI operations
4464 **************************************************************************/
4465 static void
4466 private_gtkconv_new(PurpleConversation *conv, gboolean hidden)
4468 PidginConversation *gtkconv;
4469 GtkWidget *pane = NULL;
4470 GtkWidget *tab_cont;
4471 PurpleBlistNode *convnode;
4472 GtkTargetList *targets;
4474 if (PURPLE_IS_IM_CONVERSATION(conv) && (gtkconv = pidgin_conv_find_gtkconv(conv))) {
4475 purple_conversation_set_ui_data(conv, gtkconv);
4476 if (!g_list_find(gtkconv->convs, conv))
4477 gtkconv->convs = g_list_prepend(gtkconv->convs, conv);
4478 pidgin_conv_switch_active_conversation(conv);
4479 return;
4482 gtkconv = g_new0(PidginConversation, 1);
4483 purple_conversation_set_ui_data(conv, gtkconv);
4484 gtkconv->active_conv = conv;
4485 gtkconv->convs = g_list_prepend(gtkconv->convs, conv);
4486 gtkconv->send_history = g_list_append(NULL, NULL);
4488 /* Setup some initial variables. */
4489 gtkconv->unseen_state = PIDGIN_UNSEEN_NONE;
4490 gtkconv->unseen_count = 0;
4491 gtkconv->last_flags = 0;
4493 if (PURPLE_IS_IM_CONVERSATION(conv)) {
4494 gtkconv->u.im = g_malloc0(sizeof(PidginImPane));
4495 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
4496 gtkconv->u.chat = g_malloc0(sizeof(PidginChatPane));
4498 pane = setup_common_pane(gtkconv);
4500 if (pane == NULL) {
4501 if (PURPLE_IS_CHAT_CONVERSATION(conv))
4502 g_free(gtkconv->u.chat);
4503 else if (PURPLE_IS_IM_CONVERSATION(conv))
4504 g_free(gtkconv->u.im);
4506 g_free(gtkconv);
4507 purple_conversation_set_ui_data(conv, NULL);
4508 return;
4511 g_signal_connect(G_OBJECT(pane), "button_press_event",
4512 G_CALLBACK(ignore_middle_click), NULL);
4514 /* Setup the container for the tab. */
4515 gtkconv->tab_cont = tab_cont = gtk_box_new(GTK_ORIENTATION_VERTICAL, PIDGIN_HIG_BOX_SPACE);
4516 g_object_set_data(G_OBJECT(tab_cont), "PidginConversation", gtkconv);
4517 gtk_container_set_border_width(GTK_CONTAINER(tab_cont), PIDGIN_HIG_BOX_SPACE);
4518 gtk_box_pack_start(GTK_BOX(tab_cont), pane, TRUE, TRUE, 0);
4519 gtk_widget_show(pane);
4521 convnode = get_conversation_blist_node(conv);
4522 if (convnode == NULL || !purple_blist_node_get_bool(convnode, "gtk-mute-sound"))
4523 gtkconv->make_sound = TRUE;
4525 if (convnode != NULL && purple_blist_node_has_setting(convnode, "enable-logging")) {
4526 gboolean logging = purple_blist_node_get_bool(convnode, "enable-logging");
4527 purple_conversation_set_logging(conv, logging);
4530 talkatu_editor_set_toolbar_visible(
4531 TALKATU_EDITOR(gtkconv->editor),
4532 purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/show_formatting_toolbar")
4535 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/im/show_buddy_icons"))
4536 gtk_widget_show(gtkconv->infopane_hbox);
4537 else
4538 gtk_widget_hide(gtkconv->infopane_hbox);
4541 g_signal_connect_swapped(G_OBJECT(pane), "focus",
4542 G_CALLBACK(gtk_widget_grab_focus),
4543 gtkconv->editor);
4545 if (hidden)
4546 pidgin_conv_window_add_gtkconv(hidden_convwin, gtkconv);
4547 else
4548 pidgin_conv_placement_place(gtkconv);
4550 if (generated_nick_colors == NULL) {
4551 GdkColor color;
4552 GdkRGBA rgba;
4553 color = gtk_widget_get_style(gtkconv->history)->base[GTK_STATE_NORMAL];
4554 rgba.red = color.red / 65535.0;
4555 rgba.green = color.green / 65535.0;
4556 rgba.blue = color.blue / 65535.0;
4557 rgba.alpha = 1.0;
4558 generated_nick_colors = generate_nick_colors(NICK_COLOR_GENERATE_COUNT, rgba);
4561 gtkconv->nick_colors = g_array_ref(generated_nick_colors);
4564 static void
4565 pidgin_conv_new_hidden(PurpleConversation *conv)
4567 private_gtkconv_new(conv, TRUE);
4570 void
4571 pidgin_conv_new(PurpleConversation *conv)
4573 private_gtkconv_new(conv, FALSE);
4574 if (PIDGIN_IS_PIDGIN_CONVERSATION(conv))
4575 purple_signal_emit(pidgin_conversations_get_handle(),
4576 "conversation-displayed", PIDGIN_CONVERSATION(conv));
4579 static void
4580 received_im_msg_cb(PurpleAccount *account, char *sender, char *message,
4581 PurpleConversation *conv, PurpleMessageFlags flags)
4583 PurpleConversationUiOps *ui_ops = pidgin_conversations_get_conv_ui_ops();
4584 gboolean hide = FALSE;
4585 guint timer;
4587 /* create hidden conv if hide_new pref is always */
4588 if (purple_strequal(purple_prefs_get_string(PIDGIN_PREFS_ROOT "/conversations/im/hide_new"), "always"))
4589 hide = TRUE;
4591 /* create hidden conv if hide_new pref is away and account is away */
4592 if (purple_strequal(purple_prefs_get_string(PIDGIN_PREFS_ROOT "/conversations/im/hide_new"), "away") &&
4593 !purple_status_is_available(purple_account_get_active_status(account)))
4594 hide = TRUE;
4596 if (conv && PIDGIN_IS_PIDGIN_CONVERSATION(conv) && !hide) {
4597 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
4598 if (gtkconv->win == hidden_convwin) {
4599 pidgin_conv_attach_to_conversation(gtkconv->active_conv);
4601 return;
4604 if (hide) {
4605 ui_ops->create_conversation = pidgin_conv_new_hidden;
4606 purple_im_conversation_new(account, sender);
4607 ui_ops->create_conversation = pidgin_conv_new;
4610 /* Somebody wants to keep this conversation around, so don't time it out */
4611 if (conv) {
4612 timer = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(conv), "close-timer"));
4613 if (timer) {
4614 g_source_remove(timer);
4615 g_object_set_data(G_OBJECT(conv), "close-timer", GINT_TO_POINTER(0));
4620 static void
4621 pidgin_conv_destroy(PurpleConversation *conv)
4623 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
4625 gtkconv->convs = g_list_remove(gtkconv->convs, conv);
4626 /* Don't destroy ourselves until all our convos are gone */
4627 if (gtkconv->convs) {
4628 /* Make sure the destroyed conversation is not the active one */
4629 if (gtkconv->active_conv == conv) {
4630 gtkconv->active_conv = gtkconv->convs->data;
4631 purple_conversation_update(gtkconv->active_conv, PURPLE_CONVERSATION_UPDATE_FEATURES);
4633 return;
4636 pidgin_conv_window_remove_gtkconv(gtkconv->win, gtkconv);
4638 /* If the "Save Conversation" or "Save Icon" dialogs are open then close them */
4639 purple_request_close_with_handle(gtkconv);
4640 purple_notify_close_with_handle(gtkconv);
4642 gtk_widget_destroy(gtkconv->tab_cont);
4643 g_object_unref(gtkconv->tab_cont);
4645 if (PURPLE_IS_IM_CONVERSATION(conv)) {
4646 if (gtkconv->u.im->icon_timer != 0)
4647 g_source_remove(gtkconv->u.im->icon_timer);
4649 if (gtkconv->u.im->anim != NULL)
4650 g_object_unref(G_OBJECT(gtkconv->u.im->anim));
4652 if (gtkconv->u.im->typing_timer != 0)
4653 g_source_remove(gtkconv->u.im->typing_timer);
4655 g_free(gtkconv->u.im);
4656 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
4657 purple_signals_disconnect_by_handle(gtkconv->u.chat);
4658 g_free(gtkconv->u.chat);
4661 gtkconv->send_history = g_list_first(gtkconv->send_history);
4662 g_list_foreach(gtkconv->send_history, (GFunc)g_free, NULL);
4663 g_list_free(gtkconv->send_history);
4665 if (gtkconv->attach_timer) {
4666 g_source_remove(gtkconv->attach_timer);
4669 g_array_unref(gtkconv->nick_colors);
4671 g_free(gtkconv);
4674 #if 0
4675 static const char *
4676 get_text_tag_color(GtkTextTag *tag)
4678 GdkRGBA *color = NULL;
4679 gboolean set = FALSE;
4680 static char colcode[] = "#XXXXXX";
4681 if (tag)
4682 g_object_get(G_OBJECT(tag), "foreground-set", &set, "foreground-rgba", &color, NULL);
4683 if (set && color)
4684 g_snprintf(colcode, sizeof(colcode), "#%02x%02x%02x",
4685 (unsigned int)(color->red * 255),
4686 (unsigned int)(color->green * 255),
4687 (unsigned int)(color->blue * 255));
4688 else
4689 colcode[0] = '\0';
4690 if (color)
4691 gdk_rgba_free(color);
4692 return colcode;
4695 /* The callback for an event on a link tag. */
4696 static gboolean buddytag_event(GtkTextTag *tag, GObject *imhtml,
4697 GdkEvent *event, GtkTextIter *arg2, gpointer data)
4699 if (event->type == GDK_BUTTON_PRESS
4700 || event->type == GDK_2BUTTON_PRESS) {
4701 GdkEventButton *btn_event = (GdkEventButton*) event;
4702 PurpleConversation *conv = data;
4703 char *buddyname;
4704 gchar *name;
4706 g_object_get(G_OBJECT(tag), "name", &name, NULL);
4708 /* strlen("BUDDY " or "HILIT ") == 6 */
4709 g_return_val_if_fail((name != NULL) && (strlen(name) > 6), FALSE);
4711 buddyname = name + 6;
4713 /* emit chat-nick-clicked signal */
4714 if (event->type == GDK_BUTTON_PRESS) {
4715 gint plugin_return = GPOINTER_TO_INT(purple_signal_emit_return_1(
4716 pidgin_conversations_get_handle(), "chat-nick-clicked",
4717 data, buddyname, btn_event->button));
4718 if (plugin_return) {
4719 g_free(name);
4720 return TRUE;
4724 if (btn_event->button == GDK_BUTTON_PRIMARY && event->type == GDK_2BUTTON_PRESS) {
4725 chat_do_im(PIDGIN_CONVERSATION(conv), buddyname);
4726 g_free(name);
4728 return TRUE;
4729 } else if (btn_event->button == GDK_BUTTON_MIDDLE && event->type == GDK_2BUTTON_PRESS) {
4730 chat_do_info(PIDGIN_CONVERSATION(conv), buddyname);
4731 g_free(name);
4733 return TRUE;
4734 } else if (gdk_event_triggers_context_menu(event)) {
4735 GtkTextIter start, end;
4737 /* we shouldn't display the popup
4738 * if the user has selected something: */
4739 if (!gtk_text_buffer_get_selection_bounds(
4740 gtk_text_iter_get_buffer(arg2),
4741 &start, &end)) {
4742 GtkWidget *menu = NULL;
4743 PurpleConnection *gc =
4744 purple_conversation_get_connection(conv);
4746 menu = create_chat_menu(conv, buddyname, gc);
4747 gtk_menu_popup_at_pointer(GTK_MENU(menu), event);
4749 g_free(name);
4751 /* Don't propagate the event any further */
4752 return TRUE;
4756 g_free(name);
4759 return FALSE;
4761 #endif
4763 static GtkTextTag *get_buddy_tag(PurpleChatConversation *chat, const char *who, PurpleMessageFlags flag,
4764 gboolean create)
4766 /* TODO WEBKIT */
4767 #if 0
4768 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
4769 GtkTextTag *buddytag;
4770 gchar *str;
4771 gboolean highlight = (flag & PURPLE_MESSAGE_NICK);
4772 GtkTextBuffer *buffer = GTK_IMHTML(gtkconv->imhtml)->text_buffer;
4774 str = g_strdup_printf(highlight ? "HILIT %s" : "BUDDY %s", who);
4776 buddytag = gtk_text_tag_table_lookup(
4777 gtk_text_buffer_get_tag_table(buffer), str);
4779 if (buddytag == NULL && create) {
4780 if (highlight)
4781 buddytag = gtk_text_buffer_create_tag(buffer, str,
4782 "foreground", get_text_tag_color(gtk_text_tag_table_lookup(
4783 gtk_text_buffer_get_tag_table(buffer), "highlight-name")),
4784 "weight", PANGO_WEIGHT_BOLD,
4785 NULL);
4786 else
4787 buddytag = gtk_text_buffer_create_tag(
4788 buffer, str,
4789 "foreground-rgba", get_nick_color(gtkconv, who),
4790 "weight", purple_blist_find_buddy(purple_conversation_get_account(conv), who) ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL,
4791 NULL);
4793 g_object_set_data(G_OBJECT(buddytag), "cursor", "");
4794 g_signal_connect(G_OBJECT(buddytag), "event",
4795 G_CALLBACK(buddytag_event), conv);
4798 g_free(str);
4800 return buddytag;
4801 #endif /* if 0 */
4802 return NULL;
4805 static gboolean
4806 writing_msg(PurpleConversation *conv, PurpleMessage *msg, gpointer _unused)
4808 PidginConversation *gtkconv;
4810 g_return_val_if_fail(msg != NULL, FALSE);
4812 if (!(purple_message_get_flags(msg) & PURPLE_MESSAGE_ACTIVE_ONLY))
4813 return FALSE;
4815 g_return_val_if_fail(conv != NULL, FALSE);
4816 gtkconv = PIDGIN_CONVERSATION(conv);
4817 g_return_val_if_fail(gtkconv != NULL, FALSE);
4819 if (conv == gtkconv->active_conv)
4820 return FALSE;
4822 purple_debug_info("gtkconv",
4823 "Suppressing message for an inactive conversation");
4825 return TRUE;
4828 static void
4829 pidgin_conv_write_conv(PurpleConversation *conv, PurpleMessage *pmsg)
4831 PidginMessage *pidgin_msg = NULL;
4832 PurpleMessageFlags flags;
4833 PidginConversation *gtkconv;
4834 PurpleConnection *gc;
4835 PurpleAccount *account;
4836 gboolean plugin_return;
4838 g_return_if_fail(conv != NULL);
4839 gtkconv = PIDGIN_CONVERSATION(conv);
4840 g_return_if_fail(gtkconv != NULL);
4841 flags = purple_message_get_flags(pmsg);
4843 #if 0
4844 if (gtkconv->attach_timer) {
4845 /* We are currently in the process of filling up the buffer with the message
4846 * history of the conversation. So we do not need to add the message here.
4847 * Instead, this message will be added to the message-list, which in turn will
4848 * be processed and displayed by the attach-callback.
4850 return;
4853 if (conv != gtkconv->active_conv)
4855 /* Set the active conversation to the one that just messaged us. */
4856 /* TODO: consider not doing this if the account is offline or something */
4857 if (flags & (PURPLE_MESSAGE_SEND | PURPLE_MESSAGE_RECV))
4858 pidgin_conv_switch_active_conversation(conv);
4860 #endif
4862 account = purple_conversation_get_account(conv);
4863 g_return_if_fail(account != NULL);
4864 gc = purple_account_get_connection(account);
4865 g_return_if_fail(gc != NULL || !(flags & (PURPLE_MESSAGE_SEND | PURPLE_MESSAGE_RECV)));
4867 plugin_return = GPOINTER_TO_INT(purple_signal_emit_return_1(
4868 pidgin_conversations_get_handle(),
4869 (PURPLE_IS_IM_CONVERSATION(conv) ? "displaying-im-msg" : "displaying-chat-msg"),
4870 conv, pmsg));
4871 if (plugin_return)
4873 return;
4876 pidgin_msg = pidgin_message_new(pmsg);
4877 talkatu_history_buffer_write_message(
4878 TALKATU_HISTORY_BUFFER(gtkconv->history_buffer),
4879 TALKATU_MESSAGE(pidgin_msg)
4882 /* Tab highlighting stuff */
4883 if (!(flags & PURPLE_MESSAGE_SEND) && !pidgin_conv_has_focus(conv))
4885 PidginUnseenState unseen = PIDGIN_UNSEEN_NONE;
4887 if ((flags & PURPLE_MESSAGE_NICK) == PURPLE_MESSAGE_NICK)
4888 unseen = PIDGIN_UNSEEN_NICK;
4889 else if (((flags & PURPLE_MESSAGE_SYSTEM) == PURPLE_MESSAGE_SYSTEM) ||
4890 ((flags & PURPLE_MESSAGE_ERROR) == PURPLE_MESSAGE_ERROR))
4891 unseen = PIDGIN_UNSEEN_EVENT;
4892 else if ((flags & PURPLE_MESSAGE_NO_LOG) == PURPLE_MESSAGE_NO_LOG)
4893 unseen = PIDGIN_UNSEEN_NO_LOG;
4894 else
4895 unseen = PIDGIN_UNSEEN_TEXT;
4897 gtkconv_set_unseen(gtkconv, unseen);
4900 /* on rejoin only request message history from after this message */
4901 if (flags & (PURPLE_MESSAGE_SEND | PURPLE_MESSAGE_RECV) &&
4902 PURPLE_IS_CHAT_CONVERSATION(conv)) {
4903 PurpleChat *chat = purple_blist_find_chat(
4904 purple_conversation_get_account(conv),
4905 purple_conversation_get_name(conv));
4906 if (chat) {
4907 GHashTable *comps = purple_chat_get_components(chat);
4908 time_t now, history_since, prev_history_since = 0;
4909 struct tm *history_since_tm;
4910 const char *history_since_s, *prev_history_since_s;
4912 history_since = purple_message_get_time(pmsg) + 1;
4914 prev_history_since_s = g_hash_table_lookup(comps,
4915 "history_since");
4916 if (prev_history_since_s != NULL)
4917 prev_history_since = purple_str_to_time(
4918 prev_history_since_s, TRUE, NULL, NULL,
4919 NULL);
4921 now = time(NULL);
4922 /* in case of incorrectly stored timestamps */
4923 if (prev_history_since > now)
4924 prev_history_since = now;
4925 /* in case of delayed messages */
4926 if (history_since < prev_history_since)
4927 history_since = prev_history_since;
4929 history_since_tm = gmtime(&history_since);
4930 history_since_s = purple_utf8_strftime(
4931 "%Y-%m-%dT%H:%M:%SZ", history_since_tm);
4932 if (!purple_strequal(prev_history_since_s,
4933 history_since_s))
4934 g_hash_table_replace(comps,
4935 g_strdup("history_since"),
4936 g_strdup(history_since_s));
4940 purple_signal_emit(pidgin_conversations_get_handle(),
4941 (PURPLE_IS_IM_CONVERSATION(conv) ? "displayed-im-msg" : "displayed-chat-msg"),
4942 conv, pmsg);
4943 update_typing_message(gtkconv, NULL);
4946 static gboolean get_iter_from_chatuser(PurpleChatUser *cb, GtkTreeIter *iter)
4948 GtkTreeRowReference *ref;
4949 GtkTreePath *path;
4950 GtkTreeModel *model;
4952 g_return_val_if_fail(cb != NULL, FALSE);
4954 ref = purple_chat_user_get_ui_data(cb);
4955 if (!ref)
4956 return FALSE;
4958 if ((path = gtk_tree_row_reference_get_path(ref)) == NULL)
4959 return FALSE;
4961 model = gtk_tree_row_reference_get_model(ref);
4962 if (!gtk_tree_model_get_iter(GTK_TREE_MODEL(model), iter, path)) {
4963 gtk_tree_path_free(path);
4964 return FALSE;
4967 gtk_tree_path_free(path);
4968 return TRUE;
4971 static void
4972 pidgin_conv_chat_add_users(PurpleChatConversation *chat, GList *cbuddies, gboolean new_arrivals)
4974 PidginConversation *gtkconv;
4975 PidginChatPane *gtkchat;
4976 GtkListStore *ls;
4977 GList *l;
4979 char tmp[BUF_LONG];
4980 int num_users;
4982 gtkconv = PIDGIN_CONVERSATION(PURPLE_CONVERSATION(chat));
4983 gtkchat = gtkconv->u.chat;
4985 num_users = purple_chat_conversation_get_users_count(chat);
4987 g_snprintf(tmp, sizeof(tmp),
4988 ngettext("%d person in room", "%d people in room",
4989 num_users),
4990 num_users);
4992 gtk_label_set_text(GTK_LABEL(gtkchat->count), tmp);
4994 ls = GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list)));
4996 gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(ls), GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID,
4997 GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID);
4999 l = cbuddies;
5000 while (l != NULL) {
5001 add_chat_user_common(chat, (PurpleChatUser *)l->data, NULL);
5002 l = l->next;
5005 /* Currently GTK+ maintains our sorted list after it's in the tree.
5006 * This may change if it turns out we can manage it faster ourselves.
5008 gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(ls), CHAT_USERS_ALIAS_KEY_COLUMN,
5009 GTK_SORT_ASCENDING);
5012 static void
5013 pidgin_conv_chat_rename_user(PurpleChatConversation *chat, const char *old_name,
5014 const char *new_name, const char *new_alias)
5016 PidginConversation *gtkconv;
5017 PidginChatPane *gtkchat;
5018 PurpleChatUser *old_chatuser, *new_chatuser;
5019 GtkTreeIter iter;
5020 GtkTreeModel *model;
5021 GtkTextTag *tag;
5023 gtkconv = PIDGIN_CONVERSATION(PURPLE_CONVERSATION(chat));
5024 gtkchat = gtkconv->u.chat;
5026 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
5028 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &iter))
5029 return;
5031 if ((tag = get_buddy_tag(chat, old_name, 0, FALSE)))
5032 g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_ITALIC, NULL);
5033 if ((tag = get_buddy_tag(chat, old_name, PURPLE_MESSAGE_NICK, FALSE)))
5034 g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_ITALIC, NULL);
5036 old_chatuser = purple_chat_conversation_find_user(chat, old_name);
5037 if (!old_chatuser)
5038 return;
5040 if (get_iter_from_chatuser(old_chatuser, &iter)) {
5041 GtkTreeRowReference *ref = purple_chat_user_get_ui_data(old_chatuser);
5043 gtk_list_store_remove(GTK_LIST_STORE(model), &iter);
5044 gtk_tree_row_reference_free(ref);
5045 purple_chat_user_set_ui_data(old_chatuser, NULL);
5048 g_return_if_fail(new_alias != NULL);
5050 new_chatuser = purple_chat_conversation_find_user(chat, new_name);
5052 add_chat_user_common(chat, new_chatuser, old_name);
5055 static void
5056 pidgin_conv_chat_remove_users(PurpleChatConversation *chat, GList *users)
5058 PidginConversation *gtkconv;
5059 PidginChatPane *gtkchat;
5060 GtkTreeIter iter;
5061 GtkTreeModel *model;
5062 GList *l;
5063 char tmp[BUF_LONG];
5064 int num_users;
5065 gboolean f;
5066 GtkTextTag *tag;
5068 gtkconv = PIDGIN_CONVERSATION(PURPLE_CONVERSATION(chat));
5069 gtkchat = gtkconv->u.chat;
5071 num_users = purple_chat_conversation_get_users_count(chat);
5073 for (l = users; l != NULL; l = l->next) {
5074 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
5076 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &iter))
5077 /* XXX: Break? */
5078 continue;
5080 do {
5081 char *val;
5083 gtk_tree_model_get(GTK_TREE_MODEL(model), &iter,
5084 CHAT_USERS_NAME_COLUMN, &val, -1);
5086 if (!purple_utf8_strcasecmp((char *)l->data, val)) {
5087 f = gtk_list_store_remove(GTK_LIST_STORE(model), &iter);
5089 else
5090 f = gtk_tree_model_iter_next(GTK_TREE_MODEL(model), &iter);
5092 g_free(val);
5093 } while (f);
5095 if ((tag = get_buddy_tag(chat, l->data, 0, FALSE)))
5096 g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_ITALIC, NULL);
5097 if ((tag = get_buddy_tag(chat, l->data, PURPLE_MESSAGE_NICK, FALSE)))
5098 g_object_set(G_OBJECT(tag), "style", PANGO_STYLE_ITALIC, NULL);
5101 g_snprintf(tmp, sizeof(tmp),
5102 ngettext("%d person in room", "%d people in room",
5103 num_users), num_users);
5105 gtk_label_set_text(GTK_LABEL(gtkchat->count), tmp);
5108 static void
5109 pidgin_conv_chat_update_user(PurpleChatUser *chatuser)
5111 PurpleChatConversation *chat;
5112 PidginConversation *gtkconv;
5113 PidginChatPane *gtkchat;
5114 GtkTreeIter iter;
5115 GtkTreeModel *model;
5117 if (!chatuser)
5118 return;
5120 chat = purple_chat_user_get_chat(chatuser);
5121 gtkconv = PIDGIN_CONVERSATION(PURPLE_CONVERSATION(chat));
5122 gtkchat = gtkconv->u.chat;
5124 model = gtk_tree_view_get_model(GTK_TREE_VIEW(gtkchat->list));
5126 if (!gtk_tree_model_get_iter_first(GTK_TREE_MODEL(model), &iter))
5127 return;
5129 if (get_iter_from_chatuser(chatuser, &iter)) {
5130 GtkTreeRowReference *ref = purple_chat_user_get_ui_data(chatuser);
5131 gtk_list_store_remove(GTK_LIST_STORE(model), &iter);
5132 gtk_tree_row_reference_free(ref);
5133 purple_chat_user_set_ui_data(chatuser, NULL);
5136 if (chatuser)
5137 add_chat_user_common(chat, chatuser, NULL);
5140 gboolean
5141 pidgin_conv_has_focus(PurpleConversation *conv)
5143 PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
5144 PidginConvWindow *win;
5145 gboolean has_focus;
5147 win = gtkconv->win;
5149 g_object_get(G_OBJECT(win->window), "has-toplevel-focus", &has_focus, NULL);
5151 if (has_focus && pidgin_conv_window_is_active_conversation(conv))
5152 return TRUE;
5154 return FALSE;
5158 * Makes sure all the menu items and all the buttons are hidden/shown and
5159 * sensitive/insensitive. This is called after changing tabs and when an
5160 * account signs on or off.
5162 static void
5163 gray_stuff_out(PidginConversation *gtkconv)
5165 PidginConvWindow *win;
5166 PurpleConversation *conv = gtkconv->active_conv;
5167 PurpleConnection *gc;
5168 PurpleProtocol *protocol = NULL;
5169 GdkPixbuf *window_icon = NULL;
5170 // PidginWebViewButtons buttons;
5171 PurpleAccount *account;
5173 win = pidgin_conv_get_window(gtkconv);
5174 gc = purple_conversation_get_connection(conv);
5175 account = purple_conversation_get_account(conv);
5177 if (gc != NULL)
5178 protocol = purple_connection_get_protocol(gc);
5180 if (win->menu->send_to != NULL)
5181 update_send_to_selection(win);
5184 * Handle hiding and showing stuff based on what type of conv this is.
5185 * Stuff that Purple IMs support in general should be shown for IM
5186 * conversations. Stuff that Purple chats support in general should be
5187 * shown for chat conversations. It doesn't matter whether the protocol
5188 * supports it or not--that only affects if the button or menu item
5189 * is sensitive or not.
5191 if (PURPLE_IS_IM_CONVERSATION(conv)) {
5192 /* Show stuff that applies to IMs, hide stuff that applies to chats */
5194 /* Deal with menu items */
5195 gtk_action_set_visible(win->menu->view_log, TRUE);
5196 gtk_action_set_visible(win->menu->send_file, TRUE);
5197 gtk_action_set_visible(win->menu->get_attention, TRUE);
5198 gtk_action_set_visible(win->menu->add_pounce, TRUE);
5199 gtk_action_set_visible(win->menu->get_info, TRUE);
5200 gtk_action_set_visible(win->menu->invite, FALSE);
5201 gtk_action_set_visible(win->menu->alias, TRUE);
5202 if (purple_account_privacy_check(account, purple_conversation_get_name(conv))) {
5203 gtk_action_set_visible(win->menu->unblock, FALSE);
5204 gtk_action_set_visible(win->menu->block, TRUE);
5205 } else {
5206 gtk_action_set_visible(win->menu->block, FALSE);
5207 gtk_action_set_visible(win->menu->unblock, TRUE);
5210 if (purple_blist_find_buddy(account, purple_conversation_get_name(conv)) == NULL) {
5211 gtk_action_set_visible(win->menu->add, TRUE);
5212 gtk_action_set_visible(win->menu->remove, FALSE);
5213 } else {
5214 gtk_action_set_visible(win->menu->remove, TRUE);
5215 gtk_action_set_visible(win->menu->add, FALSE);
5218 gtk_action_set_visible(win->menu->insert_link, TRUE);
5219 gtk_action_set_visible(win->menu->insert_image, TRUE);
5220 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
5221 /* Show stuff that applies to Chats, hide stuff that applies to IMs */
5223 /* Deal with menu items */
5224 gtk_action_set_visible(win->menu->view_log, TRUE);
5225 gtk_action_set_visible(win->menu->send_file, FALSE);
5226 gtk_action_set_visible(win->menu->get_attention, FALSE);
5227 gtk_action_set_visible(win->menu->add_pounce, FALSE);
5228 gtk_action_set_visible(win->menu->get_info, FALSE);
5229 gtk_action_set_visible(win->menu->invite, TRUE);
5230 gtk_action_set_visible(win->menu->alias, TRUE);
5231 gtk_action_set_visible(win->menu->block, FALSE);
5232 gtk_action_set_visible(win->menu->unblock, FALSE);
5234 if ((account == NULL) || purple_blist_find_chat(account, purple_conversation_get_name(conv)) == NULL) {
5235 /* If the chat is NOT in the buddy list */
5236 gtk_action_set_visible(win->menu->add, TRUE);
5237 gtk_action_set_visible(win->menu->remove, FALSE);
5238 } else {
5239 /* If the chat IS in the buddy list */
5240 gtk_action_set_visible(win->menu->add, FALSE);
5241 gtk_action_set_visible(win->menu->remove, TRUE);
5244 gtk_action_set_visible(win->menu->insert_link, TRUE);
5245 gtk_action_set_visible(win->menu->insert_image, TRUE);
5249 * Handle graying stuff out based on whether an account is connected
5250 * and what features that account supports.
5252 if ((gc != NULL) &&
5253 (!PURPLE_IS_CHAT_CONVERSATION(conv) ||
5254 !purple_chat_conversation_has_left(PURPLE_CHAT_CONVERSATION(conv)) ))
5256 PurpleConnectionFlags features = purple_conversation_get_features(conv);
5257 /* Account is online */
5258 /* Deal with the toolbar */
5259 #if 0
5260 if (features & PURPLE_CONNECTION_FLAG_HTML)
5262 buttons = PIDGIN_WEBVIEW_ALL; /* Everything on */
5263 if (features & PURPLE_CONNECTION_FLAG_NO_BGCOLOR)
5264 buttons &= ~PIDGIN_WEBVIEW_BACKCOLOR;
5265 if (features & PURPLE_CONNECTION_FLAG_NO_FONTSIZE)
5267 buttons &= ~PIDGIN_WEBVIEW_GROW;
5268 buttons &= ~PIDGIN_WEBVIEW_SHRINK;
5270 if (features & PURPLE_CONNECTION_FLAG_NO_URLDESC)
5271 buttons &= ~PIDGIN_WEBVIEW_LINKDESC
5272 } else {
5273 buttons = PIDGIN_WEBVIEW_SMILEY | PIDGIN_WEBVIEW_IMAGE;
5276 if (features & PURPLE_CONNECTION_FLAG_NO_IMAGES)
5277 buttons &= ~PIDGIN_WEBVIEW_IMAGE;
5279 if (features & PURPLE_CONNECTION_FLAG_ALLOW_CUSTOM_SMILEY)
5280 buttons |= PIDGIN_WEBVIEW_CUSTOM_SMILEY;
5281 else
5282 buttons &= ~PIDGIN_WEBVIEW_CUSTOM_SMILEY;
5284 pidgin_webview_set_format_functions(PIDGIN_WEBVIEW(gtkconv->entry), buttons);
5285 #endif
5287 /* Deal with menu items */
5288 gtk_action_set_sensitive(win->menu->view_log, TRUE);
5289 gtk_action_set_sensitive(win->menu->add_pounce, TRUE);
5290 gtk_action_set_sensitive(win->menu->get_info, (PURPLE_PROTOCOL_IMPLEMENTS(protocol, SERVER, get_info)));
5291 gtk_action_set_sensitive(win->menu->invite, (PURPLE_PROTOCOL_IMPLEMENTS(protocol, CHAT, invite)));
5292 gtk_action_set_sensitive(win->menu->insert_link, (features & PURPLE_CONNECTION_FLAG_HTML));
5293 gtk_action_set_sensitive(win->menu->insert_image, !(features & PURPLE_CONNECTION_FLAG_NO_IMAGES));
5295 if (PURPLE_IS_IM_CONVERSATION(conv))
5297 gboolean can_send_file = FALSE;
5298 const gchar *name = purple_conversation_get_name(conv);
5300 if (PURPLE_IS_PROTOCOL_XFER(protocol) &&
5301 purple_protocol_xfer_can_receive(PURPLE_PROTOCOL_XFER(protocol), gc, name)
5303 can_send_file = TRUE;
5306 gtk_action_set_sensitive(win->menu->add, (PURPLE_PROTOCOL_IMPLEMENTS(protocol, SERVER, add_buddy)));
5307 gtk_action_set_sensitive(win->menu->remove, (PURPLE_PROTOCOL_IMPLEMENTS(protocol, SERVER, remove_buddy)));
5308 gtk_action_set_sensitive(win->menu->send_file, can_send_file);
5309 gtk_action_set_sensitive(win->menu->get_attention, (PURPLE_IS_PROTOCOL_ATTENTION(protocol)));
5310 gtk_action_set_sensitive(win->menu->alias,
5311 (account != NULL) &&
5312 (purple_blist_find_buddy(account, purple_conversation_get_name(conv)) != NULL));
5314 else if (PURPLE_IS_CHAT_CONVERSATION(conv))
5316 gtk_action_set_sensitive(win->menu->add, (PURPLE_PROTOCOL_IMPLEMENTS(protocol, CHAT, join)));
5317 gtk_action_set_sensitive(win->menu->remove, (PURPLE_PROTOCOL_IMPLEMENTS(protocol, CHAT, join)));
5318 gtk_action_set_sensitive(win->menu->alias,
5319 (account != NULL) &&
5320 (purple_blist_find_chat(account, purple_conversation_get_name(conv)) != NULL));
5323 } else {
5324 /* Account is offline */
5325 /* Or it's a chat that we've left. */
5327 /* Then deal with menu items */
5328 gtk_action_set_sensitive(win->menu->view_log, TRUE);
5329 gtk_action_set_sensitive(win->menu->send_file, FALSE);
5330 gtk_action_set_sensitive(win->menu->get_attention, FALSE);
5331 gtk_action_set_sensitive(win->menu->add_pounce, TRUE);
5332 gtk_action_set_sensitive(win->menu->get_info, FALSE);
5333 gtk_action_set_sensitive(win->menu->invite, FALSE);
5334 gtk_action_set_sensitive(win->menu->alias, FALSE);
5335 gtk_action_set_sensitive(win->menu->add, FALSE);
5336 gtk_action_set_sensitive(win->menu->remove, FALSE);
5337 gtk_action_set_sensitive(win->menu->insert_link, TRUE);
5338 gtk_action_set_sensitive(win->menu->insert_image, FALSE);
5342 * Update the window's icon
5344 if (pidgin_conv_window_is_active_conversation(conv))
5346 GList *l = NULL;
5347 if (PURPLE_IS_IM_CONVERSATION(conv) &&
5348 (gtkconv->u.im->anim))
5350 PurpleBuddy *buddy = purple_blist_find_buddy(purple_conversation_get_account(conv), purple_conversation_get_name(conv));
5351 window_icon =
5352 gdk_pixbuf_animation_get_static_image(gtkconv->u.im->anim);
5354 if (buddy && !PURPLE_BUDDY_IS_ONLINE(buddy))
5355 gdk_pixbuf_saturate_and_pixelate(window_icon, window_icon, 0.0, FALSE);
5357 g_object_ref(window_icon);
5358 l = g_list_append(l, window_icon);
5359 } else {
5360 l = pidgin_conv_get_tab_icons(conv);
5362 gtk_window_set_icon_list(GTK_WINDOW(win->window), l);
5363 if (window_icon != NULL) {
5364 g_object_unref(G_OBJECT(window_icon));
5365 g_list_free(l);
5370 static void
5371 pidgin_conv_update_fields(PurpleConversation *conv, PidginConvFields fields)
5373 PidginConversation *gtkconv;
5374 PidginConvWindow *win;
5376 gtkconv = PIDGIN_CONVERSATION(conv);
5377 if (!gtkconv)
5378 return;
5379 win = pidgin_conv_get_window(gtkconv);
5380 if (!win)
5381 return;
5383 if (fields & PIDGIN_CONV_SET_TITLE)
5385 purple_conversation_autoset_title(conv);
5388 if (fields & PIDGIN_CONV_BUDDY_ICON)
5390 if (PURPLE_IS_IM_CONVERSATION(conv))
5391 pidgin_conv_update_buddy_icon(PURPLE_IM_CONVERSATION(conv));
5394 if (fields & PIDGIN_CONV_MENU)
5396 gray_stuff_out(PIDGIN_CONVERSATION(conv));
5397 generate_send_to_items(win);
5398 regenerate_plugins_items(win);
5401 if (fields & PIDGIN_CONV_E2EE)
5402 generate_e2ee_controls(win);
5404 if (fields & PIDGIN_CONV_TAB_ICON)
5406 update_tab_icon(conv);
5407 generate_send_to_items(win); /* To update the icons in SendTo menu */
5410 if ((fields & PIDGIN_CONV_TOPIC) &&
5411 PURPLE_IS_CHAT_CONVERSATION(conv))
5413 const char *topic;
5414 PidginChatPane *gtkchat = gtkconv->u.chat;
5416 if (gtkchat->topic_text != NULL)
5418 topic = purple_chat_conversation_get_topic(PURPLE_CHAT_CONVERSATION(conv));
5420 gtk_entry_set_text(GTK_ENTRY(gtkchat->topic_text), topic ? topic : "");
5421 gtk_widget_set_tooltip_text(gtkchat->topic_text,
5422 topic ? topic : "");
5426 if ((fields & PIDGIN_CONV_COLORIZE_TITLE) ||
5427 (fields & PIDGIN_CONV_SET_TITLE) ||
5428 (fields & PIDGIN_CONV_TOPIC))
5430 char *title;
5431 PurpleIMConversation *im = NULL;
5432 PurpleAccount *account = purple_conversation_get_account(conv);
5433 PurpleBuddy *buddy = NULL;
5434 char *markup = NULL;
5435 AtkObject *accessibility_obj;
5436 /* I think this is a little longer than it needs to be but I'm lazy. */
5437 char *style;
5439 if (PURPLE_IS_IM_CONVERSATION(conv))
5440 im = PURPLE_IM_CONVERSATION(conv);
5442 if ((account == NULL) ||
5443 !purple_account_is_connected(account) ||
5444 (PURPLE_IS_CHAT_CONVERSATION(conv)
5445 && purple_chat_conversation_has_left(PURPLE_CHAT_CONVERSATION(conv))))
5446 title = g_strdup_printf("(%s)", purple_conversation_get_title(conv));
5447 else
5448 title = g_strdup(purple_conversation_get_title(conv));
5450 if (PURPLE_IS_IM_CONVERSATION(conv)) {
5451 buddy = purple_blist_find_buddy(account, purple_conversation_get_name(conv));
5452 if (buddy) {
5453 markup = pidgin_blist_get_name_markup(buddy, FALSE, FALSE);
5454 } else {
5455 markup = title;
5457 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
5458 const char *topic = gtkconv->u.chat->topic_text
5459 ? gtk_entry_get_text(GTK_ENTRY(gtkconv->u.chat->topic_text))
5460 : NULL;
5461 const char *title = purple_conversation_get_title(conv);
5462 const char *name = purple_conversation_get_name(conv);
5464 char *topic_esc, *unaliased, *unaliased_esc, *title_esc;
5466 topic_esc = topic ? g_markup_escape_text(topic, -1) : NULL;
5467 unaliased = g_utf8_collate(title, name) ? g_strdup_printf("(%s)", name) : NULL;
5468 unaliased_esc = unaliased ? g_markup_escape_text(unaliased, -1) : NULL;
5469 title_esc = g_markup_escape_text(title, -1);
5471 markup = g_strdup_printf("%s%s<span size='smaller'>%s</span>%s<span color='%s' size='smaller'>%s</span>",
5472 title_esc,
5473 unaliased_esc ? " " : "",
5474 unaliased_esc ? unaliased_esc : "",
5475 topic_esc && *topic_esc ? "\n" : "",
5476 pidgin_get_dim_grey_string(gtkconv->infopane),
5477 topic_esc ? topic_esc : "");
5479 g_free(title_esc);
5480 g_free(topic_esc);
5481 g_free(unaliased);
5482 g_free(unaliased_esc);
5484 gtk_list_store_set(gtkconv->infopane_model, &(gtkconv->infopane_iter),
5485 CONV_TEXT_COLUMN, markup, -1);
5486 /* XXX seanegan Why do I have to do this? */
5487 gtk_widget_queue_draw(gtkconv->infopane);
5489 if (title != markup)
5490 g_free(markup);
5492 if (!gtk_widget_get_realized(gtkconv->tab_label))
5493 gtk_widget_realize(gtkconv->tab_label);
5495 accessibility_obj = gtk_widget_get_accessible(gtkconv->tab_cont);
5496 if (im != NULL &&
5497 purple_im_conversation_get_typing_state(im) == PURPLE_IM_TYPING) {
5498 atk_object_set_description(accessibility_obj, _("Typing"));
5499 style = "tab-label-typing";
5500 } else if (im != NULL &&
5501 purple_im_conversation_get_typing_state(im) == PURPLE_IM_TYPED) {
5502 atk_object_set_description(accessibility_obj, _("Stopped Typing"));
5503 style = "tab-label-typed";
5504 } else if (gtkconv->unseen_state == PIDGIN_UNSEEN_NICK) {
5505 atk_object_set_description(accessibility_obj, _("Nick Said"));
5506 style = "tab-label-attention";
5507 } else if (gtkconv->unseen_state == PIDGIN_UNSEEN_TEXT) {
5508 atk_object_set_description(accessibility_obj, _("Unread Messages"));
5509 if (PURPLE_IS_CHAT_CONVERSATION(gtkconv->active_conv))
5510 style = "tab-label-unreadchat";
5511 else
5512 style = "tab-label-attention";
5513 } else if (gtkconv->unseen_state == PIDGIN_UNSEEN_EVENT) {
5514 atk_object_set_description(accessibility_obj, _("New Event"));
5515 style = "tab-label-event";
5516 } else {
5517 style = "tab-label";
5520 gtk_widget_set_name(gtkconv->tab_label, style);
5521 gtk_label_set_text(GTK_LABEL(gtkconv->tab_label), title);
5522 gtk_widget_set_state_flags(gtkconv->tab_label, GTK_STATE_FLAG_ACTIVE, TRUE);
5524 if (gtkconv->unseen_state == PIDGIN_UNSEEN_TEXT ||
5525 gtkconv->unseen_state == PIDGIN_UNSEEN_NICK ||
5526 gtkconv->unseen_state == PIDGIN_UNSEEN_EVENT) {
5527 PangoAttrList *list = pango_attr_list_new();
5528 PangoAttribute *attr = pango_attr_weight_new(PANGO_WEIGHT_BOLD);
5529 attr->start_index = 0;
5530 attr->end_index = -1;
5531 pango_attr_list_insert(list, attr);
5532 gtk_label_set_attributes(GTK_LABEL(gtkconv->tab_label), list);
5533 pango_attr_list_unref(list);
5534 } else
5535 gtk_label_set_attributes(GTK_LABEL(gtkconv->tab_label), NULL);
5537 if (pidgin_conv_window_is_active_conversation(conv))
5538 update_typing_icon(gtkconv);
5540 gtk_label_set_text(GTK_LABEL(gtkconv->menu_label), title);
5541 if (pidgin_conv_window_is_active_conversation(conv)) {
5542 const char* current_title = gtk_window_get_title(GTK_WINDOW(win->window));
5543 if (current_title == NULL || !purple_strequal(current_title, title))
5544 gtk_window_set_title(GTK_WINDOW(win->window), title);
5547 g_free(title);
5551 static void
5552 pidgin_conv_updated(PurpleConversation *conv, PurpleConversationUpdateType type)
5554 PidginConvFields flags = 0;
5556 g_return_if_fail(conv != NULL);
5558 if (type == PURPLE_CONVERSATION_UPDATE_ACCOUNT)
5560 flags = PIDGIN_CONV_ALL;
5562 else if (type == PURPLE_CONVERSATION_UPDATE_TYPING ||
5563 type == PURPLE_CONVERSATION_UPDATE_UNSEEN ||
5564 type == PURPLE_CONVERSATION_UPDATE_TITLE)
5566 flags = PIDGIN_CONV_COLORIZE_TITLE;
5568 else if (type == PURPLE_CONVERSATION_UPDATE_TOPIC)
5570 flags = PIDGIN_CONV_TOPIC;
5572 else if (type == PURPLE_CONVERSATION_ACCOUNT_ONLINE ||
5573 type == PURPLE_CONVERSATION_ACCOUNT_OFFLINE)
5575 flags = PIDGIN_CONV_MENU | PIDGIN_CONV_TAB_ICON | PIDGIN_CONV_SET_TITLE;
5577 else if (type == PURPLE_CONVERSATION_UPDATE_AWAY)
5579 flags = PIDGIN_CONV_TAB_ICON;
5581 else if (type == PURPLE_CONVERSATION_UPDATE_ADD ||
5582 type == PURPLE_CONVERSATION_UPDATE_REMOVE ||
5583 type == PURPLE_CONVERSATION_UPDATE_CHATLEFT)
5585 flags = PIDGIN_CONV_SET_TITLE | PIDGIN_CONV_MENU;
5587 else if (type == PURPLE_CONVERSATION_UPDATE_ICON)
5589 flags = PIDGIN_CONV_BUDDY_ICON;
5591 else if (type == PURPLE_CONVERSATION_UPDATE_FEATURES)
5593 flags = PIDGIN_CONV_MENU;
5595 else if (type == PURPLE_CONVERSATION_UPDATE_E2EE)
5597 flags = PIDGIN_CONV_E2EE | PIDGIN_CONV_MENU;
5600 pidgin_conv_update_fields(conv, flags);
5603 static void
5604 wrote_msg_update_unseen_cb(PurpleConversation *conv, PurpleMessage *msg,
5605 gpointer _unused)
5607 PidginConversation *gtkconv = conv ? PIDGIN_CONVERSATION(conv) : NULL;
5608 PurpleMessageFlags flags;
5609 if (conv == NULL || (gtkconv && gtkconv->win != hidden_convwin))
5610 return;
5611 flags = purple_message_get_flags(msg);
5612 if (flags & (PURPLE_MESSAGE_SEND | PURPLE_MESSAGE_RECV)) {
5613 PidginUnseenState unseen = PIDGIN_UNSEEN_NONE;
5615 if ((flags & PURPLE_MESSAGE_NICK) == PURPLE_MESSAGE_NICK)
5616 unseen = PIDGIN_UNSEEN_NICK;
5617 else if (((flags & PURPLE_MESSAGE_SYSTEM) == PURPLE_MESSAGE_SYSTEM) ||
5618 ((flags & PURPLE_MESSAGE_ERROR) == PURPLE_MESSAGE_ERROR))
5619 unseen = PIDGIN_UNSEEN_EVENT;
5620 else if ((flags & PURPLE_MESSAGE_NO_LOG) == PURPLE_MESSAGE_NO_LOG)
5621 unseen = PIDGIN_UNSEEN_NO_LOG;
5622 else
5623 unseen = PIDGIN_UNSEEN_TEXT;
5625 conv_set_unseen(conv, unseen);
5629 static PurpleConversationUiOps conversation_ui_ops =
5631 pidgin_conv_new,
5632 pidgin_conv_destroy, /* destroy_conversation */
5633 NULL, /* write_chat */
5634 NULL, /* write_im */
5635 pidgin_conv_write_conv, /* write_conv */
5636 pidgin_conv_chat_add_users, /* chat_add_users */
5637 pidgin_conv_chat_rename_user, /* chat_rename_user */
5638 pidgin_conv_chat_remove_users, /* chat_remove_users */
5639 pidgin_conv_chat_update_user, /* chat_update_user */
5640 pidgin_conv_present_conversation, /* present */
5641 pidgin_conv_has_focus, /* has_focus */
5642 NULL, /* send_confirm */
5643 NULL,
5644 NULL,
5645 NULL,
5646 NULL
5649 PurpleConversationUiOps *
5650 pidgin_conversations_get_conv_ui_ops(void)
5652 return &conversation_ui_ops;
5655 /**************************************************************************
5656 * Public conversation utility functions
5657 **************************************************************************/
5658 void
5659 pidgin_conv_update_buddy_icon(PurpleIMConversation *im)
5661 PidginConversation *gtkconv;
5662 PurpleConversation *conv;
5663 PidginConvWindow *win;
5665 PurpleBuddy *buddy;
5667 PurpleImage *custom_img = NULL;
5668 gconstpointer data = NULL;
5669 size_t len;
5671 GdkPixbuf *buf;
5673 GList *children;
5674 GtkWidget *event;
5675 GdkPixbuf *scale;
5676 int scale_width, scale_height;
5677 int size = 0;
5679 PurpleAccount *account;
5681 PurpleBuddyIcon *icon;
5683 conv = PURPLE_CONVERSATION(im);
5685 g_return_if_fail(conv != NULL);
5686 g_return_if_fail(PIDGIN_IS_PIDGIN_CONVERSATION(conv));
5688 gtkconv = PIDGIN_CONVERSATION(conv);
5689 win = gtkconv->win;
5690 if (conv != gtkconv->active_conv)
5691 return;
5693 if (!gtkconv->u.im->show_icon)
5694 return;
5696 account = purple_conversation_get_account(conv);
5698 /* Remove the current icon stuff */
5699 children = gtk_container_get_children(GTK_CONTAINER(gtkconv->u.im->icon_container));
5700 if (children) {
5701 /* We know there's only one child here. It'd be nice to shortcut to the
5702 event box, but we can't change the PidginConversation until 3.0 */
5703 event = (GtkWidget *)children->data;
5704 gtk_container_remove(GTK_CONTAINER(gtkconv->u.im->icon_container), event);
5705 g_list_free(children);
5708 if (gtkconv->u.im->anim != NULL)
5709 g_object_unref(G_OBJECT(gtkconv->u.im->anim));
5711 gtkconv->u.im->anim = NULL;
5713 if (gtkconv->u.im->icon_timer != 0)
5714 g_source_remove(gtkconv->u.im->icon_timer);
5716 gtkconv->u.im->icon_timer = 0;
5718 if (gtkconv->u.im->iter != NULL)
5719 g_object_unref(G_OBJECT(gtkconv->u.im->iter));
5721 gtkconv->u.im->iter = NULL;
5723 if (!purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/im/show_buddy_icons"))
5724 return;
5726 if (purple_conversation_get_connection(conv) == NULL)
5727 return;
5729 buddy = purple_blist_find_buddy(account, purple_conversation_get_name(conv));
5730 if (buddy)
5732 PurpleContact *contact = purple_buddy_get_contact(buddy);
5733 if (contact) {
5734 custom_img = purple_buddy_icons_node_find_custom_icon((PurpleBlistNode*)contact);
5735 if (custom_img) {
5736 /* There is a custom icon for this user */
5737 data = purple_image_get_data(custom_img);
5738 len = purple_image_get_data_size(custom_img);
5743 if (data == NULL) {
5744 icon = purple_im_conversation_get_icon(im);
5745 if (icon == NULL)
5747 gtk_widget_set_size_request(gtkconv->u.im->icon_container,
5748 -1, BUDDYICON_SIZE_MIN);
5749 return;
5752 data = purple_buddy_icon_get_data(icon, &len);
5753 if (data == NULL)
5755 gtk_widget_set_size_request(gtkconv->u.im->icon_container,
5756 -1, BUDDYICON_SIZE_MIN);
5757 return;
5761 gtkconv->u.im->anim = pidgin_pixbuf_anim_from_data(data, len);
5762 if (custom_img)
5763 g_object_unref(custom_img);
5765 if (!gtkconv->u.im->anim) {
5766 purple_debug_error("gtkconv", "Couldn't load icon for conv %s\n",
5767 purple_conversation_get_name(conv));
5768 return;
5771 if (gdk_pixbuf_animation_is_static_image(gtkconv->u.im->anim)) {
5772 GdkPixbuf *stat;
5773 gtkconv->u.im->iter = NULL;
5774 stat = gdk_pixbuf_animation_get_static_image(gtkconv->u.im->anim);
5775 buf = gdk_pixbuf_add_alpha(stat, FALSE, 0, 0, 0);
5776 } else {
5777 GdkPixbuf *stat;
5778 gtkconv->u.im->iter =
5779 gdk_pixbuf_animation_get_iter(gtkconv->u.im->anim, NULL); /* LEAK */
5780 stat = gdk_pixbuf_animation_iter_get_pixbuf(gtkconv->u.im->iter);
5781 buf = gdk_pixbuf_add_alpha(stat, FALSE, 0, 0, 0);
5782 if (gtkconv->u.im->animate)
5783 start_anim(NULL, gtkconv);
5786 scale_width = gdk_pixbuf_get_width(buf);
5787 scale_height = gdk_pixbuf_get_height(buf);
5789 gtk_widget_get_size_request(gtkconv->u.im->icon_container, NULL, &size);
5790 size = MIN(size, MIN(scale_width, scale_height));
5792 /* Some sanity checks */
5793 size = CLAMP(size, BUDDYICON_SIZE_MIN, BUDDYICON_SIZE_MAX);
5794 if (scale_width == scale_height) {
5795 scale_width = scale_height = size;
5796 } else if (scale_height > scale_width) {
5797 scale_width = size * scale_width / scale_height;
5798 scale_height = size;
5799 } else {
5800 scale_height = size * scale_height / scale_width;
5801 scale_width = size;
5803 scale = gdk_pixbuf_scale_simple(buf, scale_width, scale_height,
5804 GDK_INTERP_BILINEAR);
5805 g_object_unref(buf);
5806 if (pidgin_gdk_pixbuf_is_opaque(scale))
5807 pidgin_gdk_pixbuf_make_round(scale);
5809 event = gtk_event_box_new();
5810 gtk_container_add(GTK_CONTAINER(gtkconv->u.im->icon_container), event);
5811 gtk_event_box_set_visible_window(GTK_EVENT_BOX(event), FALSE);
5812 gtk_widget_add_events(event,
5813 GDK_POINTER_MOTION_MASK | GDK_LEAVE_NOTIFY_MASK);
5814 g_signal_connect(G_OBJECT(event), "button-press-event",
5815 G_CALLBACK(icon_menu), gtkconv);
5817 pidgin_tooltip_setup_for_widget(event, gtkconv, pidgin_conv_create_tooltip, NULL);
5818 gtk_widget_show(event);
5820 gtkconv->u.im->icon = gtk_image_new_from_pixbuf(scale);
5821 gtk_container_add(GTK_CONTAINER(event), gtkconv->u.im->icon);
5822 gtk_widget_show(gtkconv->u.im->icon);
5824 g_object_unref(G_OBJECT(scale));
5826 /* The buddy icon code needs badly to be fixed. */
5827 if(pidgin_conv_window_is_active_conversation(conv))
5829 buf = gdk_pixbuf_animation_get_static_image(gtkconv->u.im->anim);
5830 if (buddy && !PURPLE_BUDDY_IS_ONLINE(buddy))
5831 gdk_pixbuf_saturate_and_pixelate(buf, buf, 0.0, FALSE);
5832 gtk_window_set_icon(GTK_WINDOW(win->window), buf);
5836 void
5837 pidgin_conv_update_buttons_by_protocol(PurpleConversation *conv)
5839 PidginConvWindow *win;
5841 if (!PIDGIN_IS_PIDGIN_CONVERSATION(conv))
5842 return;
5844 win = PIDGIN_CONVERSATION(conv)->win;
5846 if (win != NULL && pidgin_conv_window_is_active_conversation(conv))
5847 gray_stuff_out(PIDGIN_CONVERSATION(conv));
5850 static gboolean
5851 pidgin_conv_xy_to_right_infopane(PidginConvWindow *win, int x, int y)
5853 gint pane_x, pane_y, x_rel;
5854 PidginConversation *gtkconv;
5855 GtkAllocation allocation;
5857 gdk_window_get_origin(gtk_widget_get_window(win->notebook),
5858 &pane_x, &pane_y);
5859 x_rel = x - pane_x;
5860 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
5861 gtk_widget_get_allocation(gtkconv->infopane, &allocation);
5862 return (x_rel > allocation.x + allocation.width / 2);
5866 pidgin_conv_get_tab_at_xy(PidginConvWindow *win, int x, int y, gboolean *to_right)
5868 gint nb_x, nb_y, x_rel, y_rel;
5869 GtkNotebook *notebook;
5870 GtkWidget *page, *tab;
5871 gint i, page_num = -1;
5872 gint count;
5873 gboolean horiz;
5875 if (to_right)
5876 *to_right = FALSE;
5878 notebook = GTK_NOTEBOOK(win->notebook);
5880 gdk_window_get_origin(gtk_widget_get_window(win->notebook), &nb_x, &nb_y);
5881 x_rel = x - nb_x;
5882 y_rel = y - nb_y;
5884 horiz = (gtk_notebook_get_tab_pos(notebook) == GTK_POS_TOP ||
5885 gtk_notebook_get_tab_pos(notebook) == GTK_POS_BOTTOM);
5887 count = gtk_notebook_get_n_pages(GTK_NOTEBOOK(notebook));
5889 for (i = 0; i < count; i++) {
5890 GtkAllocation allocation;
5892 page = gtk_notebook_get_nth_page(GTK_NOTEBOOK(notebook), i);
5893 tab = gtk_notebook_get_tab_label(GTK_NOTEBOOK(notebook), page);
5894 gtk_widget_get_allocation(tab, &allocation);
5896 /* Make sure the tab is not hidden beyond an arrow */
5897 if (!gtk_widget_is_drawable(tab) && gtk_notebook_get_show_tabs(notebook))
5898 continue;
5900 if (horiz) {
5901 if (x_rel >= allocation.x - PIDGIN_HIG_BOX_SPACE &&
5902 x_rel <= allocation.x + allocation.width + PIDGIN_HIG_BOX_SPACE) {
5903 page_num = i;
5905 if (to_right && x_rel >= allocation.x + allocation.width/2)
5906 *to_right = TRUE;
5908 break;
5910 } else {
5911 if (y_rel >= allocation.y - PIDGIN_HIG_BOX_SPACE &&
5912 y_rel <= allocation.y + allocation.height + PIDGIN_HIG_BOX_SPACE) {
5913 page_num = i;
5915 if (to_right && y_rel >= allocation.y + allocation.height/2)
5916 *to_right = TRUE;
5918 break;
5923 if (page_num == -1) {
5924 /* Add after the last tab */
5925 page_num = count - 1;
5928 return page_num;
5931 static void
5932 close_on_tabs_pref_cb(const char *name, PurplePrefType type,
5933 gconstpointer value, gpointer data)
5935 GList *l;
5936 PurpleConversation *conv;
5937 PidginConversation *gtkconv;
5939 for (l = purple_conversations_get_all(); l != NULL; l = l->next) {
5940 conv = (PurpleConversation *)l->data;
5942 if (!PIDGIN_IS_PIDGIN_CONVERSATION(conv))
5943 continue;
5945 gtkconv = PIDGIN_CONVERSATION(conv);
5947 if (value)
5948 gtk_widget_show(gtkconv->close);
5949 else
5950 gtk_widget_hide(gtkconv->close);
5954 static void
5955 spellcheck_pref_cb(const char *name, PurplePrefType type,
5956 gconstpointer value, gpointer data)
5958 GList *cl;
5959 PurpleConversation *conv;
5960 PidginConversation *gtkconv;
5962 for (cl = purple_conversations_get_all(); cl != NULL; cl = cl->next) {
5964 conv = (PurpleConversation *)cl->data;
5966 if (!PIDGIN_IS_PIDGIN_CONVERSATION(conv))
5967 continue;
5969 gtkconv = PIDGIN_CONVERSATION(conv);
5971 # warning toggle spell checking when talkatu #60 is done.
5975 static void
5976 tab_side_pref_cb(const char *name, PurplePrefType type,
5977 gconstpointer value, gpointer data)
5979 GList *gtkwins, *gtkconvs;
5980 GtkPositionType pos;
5981 PidginConvWindow *gtkwin;
5983 pos = GPOINTER_TO_INT(value);
5985 for (gtkwins = pidgin_conv_windows_get_list(); gtkwins != NULL; gtkwins = gtkwins->next) {
5986 gtkwin = gtkwins->data;
5987 gtk_notebook_set_tab_pos(GTK_NOTEBOOK(gtkwin->notebook), pos&~8);
5988 for (gtkconvs = gtkwin->gtkconvs; gtkconvs != NULL; gtkconvs = gtkconvs->next) {
5989 pidgin_conv_tab_pack(gtkwin, gtkconvs->data);
5994 static void
5995 show_formatting_toolbar_pref_cb(const char *name, PurplePrefType type,
5996 gconstpointer value, gpointer data)
5998 GList *l;
5999 PurpleConversation *conv;
6000 PidginConversation *gtkconv;
6001 PidginConvWindow *win;
6002 gboolean visible = (gboolean)GPOINTER_TO_INT(value);
6004 for (l = purple_conversations_get_all(); l != NULL; l = l->next)
6006 conv = (PurpleConversation *)l->data;
6008 if (!PIDGIN_IS_PIDGIN_CONVERSATION(conv))
6009 continue;
6011 gtkconv = PIDGIN_CONVERSATION(conv);
6012 win = gtkconv->win;
6014 gtk_toggle_action_set_active(
6015 GTK_TOGGLE_ACTION(win->menu->show_formatting_toolbar),
6016 visible
6019 talkatu_editor_set_toolbar_visible(TALKATU_EDITOR(gtkconv->editor), visible);
6023 static void
6024 animate_buddy_icons_pref_cb(const char *name, PurplePrefType type,
6025 gconstpointer value, gpointer data)
6027 GList *l;
6028 PurpleConversation *conv;
6029 PidginConversation *gtkconv;
6030 PidginConvWindow *win;
6032 if (!purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/im/show_buddy_icons"))
6033 return;
6035 /* Set the "animate" flag for each icon based on the new preference */
6036 for (l = purple_conversations_get_ims(); l != NULL; l = l->next) {
6037 conv = (PurpleConversation *)l->data;
6038 gtkconv = PIDGIN_CONVERSATION(conv);
6039 if (gtkconv)
6040 gtkconv->u.im->animate = GPOINTER_TO_INT(value);
6043 /* Now either stop or start animation for the active conversation in each window */
6044 for (l = pidgin_conv_windows_get_list(); l != NULL; l = l->next) {
6045 win = l->data;
6046 conv = pidgin_conv_window_get_active_conversation(win);
6047 pidgin_conv_update_buddy_icon(PURPLE_IM_CONVERSATION(conv));
6051 static void
6052 show_buddy_icons_pref_cb(const char *name, PurplePrefType type,
6053 gconstpointer value, gpointer data)
6055 GList *l;
6057 for (l = purple_conversations_get_all(); l != NULL; l = l->next) {
6058 PurpleConversation *conv = l->data;
6059 if (!PIDGIN_CONVERSATION(conv))
6060 continue;
6061 if (GPOINTER_TO_INT(value))
6062 gtk_widget_show(PIDGIN_CONVERSATION(conv)->infopane_hbox);
6063 else
6064 gtk_widget_hide(PIDGIN_CONVERSATION(conv)->infopane_hbox);
6066 if (PURPLE_IS_IM_CONVERSATION(conv)) {
6067 pidgin_conv_update_buddy_icon(PURPLE_IM_CONVERSATION(conv));
6071 /* Make the tabs show/hide correctly */
6072 for (l = pidgin_conv_windows_get_list(); l != NULL; l = l->next) {
6073 PidginConvWindow *win = l->data;
6074 if (pidgin_conv_window_get_gtkconv_count(win) == 1)
6075 gtk_notebook_set_show_tabs(GTK_NOTEBOOK(win->notebook),
6076 GPOINTER_TO_INT(value) == 0);
6080 static void
6081 show_protocol_icons_pref_cb(const char *name, PurplePrefType type,
6082 gconstpointer value, gpointer data)
6084 GList *l;
6085 for (l = purple_conversations_get_all(); l != NULL; l = l->next) {
6086 PurpleConversation *conv = l->data;
6087 if (PIDGIN_CONVERSATION(conv))
6088 update_tab_icon(conv);
6092 static void
6093 conv_placement_usetabs_cb(const char *name, PurplePrefType type,
6094 gconstpointer value, gpointer data)
6096 purple_prefs_trigger_callback(PIDGIN_PREFS_ROOT "/conversations/placement");
6099 static void
6100 account_status_changed_cb(PurpleAccount *account, PurpleStatus *oldstatus,
6101 PurpleStatus *newstatus)
6103 GList *l;
6104 PurpleConversation *conv = NULL;
6105 PidginConversation *gtkconv;
6107 if(!purple_strequal(purple_prefs_get_string(PIDGIN_PREFS_ROOT "/conversations/im/hide_new"), "away"))
6108 return;
6110 if(purple_status_is_available(oldstatus) || !purple_status_is_available(newstatus))
6111 return;
6113 for (l = hidden_convwin->gtkconvs; l; ) {
6114 gtkconv = l->data;
6115 l = l->next;
6117 conv = gtkconv->active_conv;
6118 if (PURPLE_IS_CHAT_CONVERSATION(conv) ||
6119 account != purple_conversation_get_account(conv))
6120 continue;
6122 pidgin_conv_attach_to_conversation(conv);
6124 /* TODO: do we need to do anything for any other conversations that are in the same gtkconv here?
6125 * I'm a little concerned that not doing so will cause the "pending" indicator in the gtkblist not to be cleared. -DAA*/
6126 purple_conversation_update(conv, PURPLE_CONVERSATION_UPDATE_UNSEEN);
6130 static void
6131 hide_new_pref_cb(const char *name, PurplePrefType type,
6132 gconstpointer value, gpointer data)
6134 GList *l;
6135 PurpleConversation *conv = NULL;
6136 PidginConversation *gtkconv;
6137 gboolean when_away = FALSE;
6139 if(!hidden_convwin)
6140 return;
6142 if(purple_strequal(purple_prefs_get_string(PIDGIN_PREFS_ROOT "/conversations/im/hide_new"), "always"))
6143 return;
6145 if(purple_strequal(purple_prefs_get_string(PIDGIN_PREFS_ROOT "/conversations/im/hide_new"), "away"))
6146 when_away = TRUE;
6148 for (l = hidden_convwin->gtkconvs; l; )
6150 gtkconv = l->data;
6151 l = l->next;
6153 conv = gtkconv->active_conv;
6155 if (PURPLE_IS_CHAT_CONVERSATION(conv) ||
6156 gtkconv->unseen_count == 0 ||
6157 (when_away && !purple_status_is_available(
6158 purple_account_get_active_status(
6159 purple_conversation_get_account(conv)))))
6160 continue;
6162 pidgin_conv_attach_to_conversation(conv);
6167 static void
6168 conv_placement_pref_cb(const char *name, PurplePrefType type,
6169 gconstpointer value, gpointer data)
6171 PidginConvPlacementFunc func;
6173 if (!purple_strequal(name, PIDGIN_PREFS_ROOT "/conversations/placement"))
6174 return;
6176 func = pidgin_conv_placement_get_fnc(value);
6178 if (func == NULL)
6179 return;
6181 pidgin_conv_placement_set_current_func(func);
6184 static PidginConversation *
6185 get_gtkconv_with_contact(PurpleContact *contact)
6187 PurpleBlistNode *node;
6189 node = ((PurpleBlistNode*)contact)->child;
6191 for (; node; node = node->next)
6193 PurpleBuddy *buddy = (PurpleBuddy*)node;
6194 PurpleIMConversation *im;
6195 im = purple_conversations_find_im_with_account(purple_buddy_get_name(buddy), purple_buddy_get_account(buddy));
6196 if (im)
6197 return PIDGIN_CONVERSATION(PURPLE_CONVERSATION(im));
6199 return NULL;
6202 static void
6203 account_signed_off_cb(PurpleConnection *gc, gpointer event)
6205 GList *iter;
6207 for (iter = purple_conversations_get_all(); iter; iter = iter->next)
6209 PurpleConversation *conv = iter->data;
6211 /* This seems fine in theory, but we also need to cover the
6212 * case of this account matching one of the other buddies in
6213 * one of the contacts containing the buddy corresponding to
6214 * a conversation. It's easier to just update them all. */
6215 /* if (purple_conversation_get_account(conv) == account) */
6216 pidgin_conv_update_fields(conv, PIDGIN_CONV_TAB_ICON |
6217 PIDGIN_CONV_MENU | PIDGIN_CONV_COLORIZE_TITLE);
6219 if (PURPLE_CONNECTION_IS_CONNECTED(gc) &&
6220 PURPLE_IS_CHAT_CONVERSATION(conv) &&
6221 purple_conversation_get_account(conv) == purple_connection_get_account(gc) &&
6222 g_object_get_data(G_OBJECT(conv), "want-to-rejoin")) {
6223 GHashTable *comps = NULL;
6224 PurpleChat *chat = purple_blist_find_chat(purple_conversation_get_account(conv), purple_conversation_get_name(conv));
6225 if (chat == NULL) {
6226 PurpleProtocol *protocol = purple_connection_get_protocol(gc);
6227 comps = purple_protocol_chat_iface_info_defaults(protocol, gc, purple_conversation_get_name(conv));
6228 } else {
6229 comps = purple_chat_get_components(chat);
6231 purple_serv_join_chat(gc, comps);
6232 if (chat == NULL && comps != NULL)
6233 g_hash_table_destroy(comps);
6238 static void
6239 account_signing_off(PurpleConnection *gc)
6241 GList *list = purple_conversations_get_chats();
6242 PurpleAccount *account = purple_connection_get_account(gc);
6244 /* We are about to sign off. See which chats we are currently in, and mark
6245 * them for rejoin on reconnect. */
6246 while (list) {
6247 PurpleConversation *conv = list->data;
6248 if (!purple_chat_conversation_has_left(PURPLE_CHAT_CONVERSATION(conv)) &&
6249 purple_conversation_get_account(conv) == account) {
6250 g_object_set_data(G_OBJECT(conv), "want-to-rejoin", GINT_TO_POINTER(TRUE));
6251 purple_conversation_write_system_message(conv,
6252 _("The account has disconnected and you are no "
6253 "longer in this chat. You will automatically "
6254 "rejoin the chat when the account reconnects."),
6255 PURPLE_MESSAGE_NO_LOG);
6257 list = list->next;
6261 static void
6262 update_buddy_status_changed(PurpleBuddy *buddy, PurpleStatus *old, PurpleStatus *newstatus)
6264 PidginConversation *gtkconv;
6265 PurpleConversation *conv;
6267 gtkconv = get_gtkconv_with_contact(purple_buddy_get_contact(buddy));
6268 if (gtkconv)
6270 conv = gtkconv->active_conv;
6271 pidgin_conv_update_fields(conv, PIDGIN_CONV_TAB_ICON
6272 | PIDGIN_CONV_COLORIZE_TITLE
6273 | PIDGIN_CONV_BUDDY_ICON);
6274 if ((purple_status_is_online(old) ^ purple_status_is_online(newstatus)) != 0)
6275 pidgin_conv_update_fields(conv, PIDGIN_CONV_MENU);
6279 static void
6280 update_buddy_privacy_changed(PurpleBuddy *buddy)
6282 PidginConversation *gtkconv;
6283 PurpleConversation *conv;
6285 gtkconv = get_gtkconv_with_contact(purple_buddy_get_contact(buddy));
6286 if (gtkconv) {
6287 conv = gtkconv->active_conv;
6288 pidgin_conv_update_fields(conv, PIDGIN_CONV_TAB_ICON | PIDGIN_CONV_MENU);
6292 static void
6293 update_buddy_idle_changed(PurpleBuddy *buddy, gboolean old, gboolean newidle)
6295 PurpleIMConversation *im;
6297 im = purple_conversations_find_im_with_account(purple_buddy_get_name(buddy), purple_buddy_get_account(buddy));
6298 if (im)
6299 pidgin_conv_update_fields(PURPLE_CONVERSATION(im), PIDGIN_CONV_TAB_ICON);
6302 static void
6303 update_buddy_icon(PurpleBuddy *buddy)
6305 PurpleIMConversation *im;
6307 im = purple_conversations_find_im_with_account(purple_buddy_get_name(buddy), purple_buddy_get_account(buddy));
6308 if (im)
6309 pidgin_conv_update_fields(PURPLE_CONVERSATION(im), PIDGIN_CONV_BUDDY_ICON);
6312 static void
6313 update_buddy_sign(PurpleBuddy *buddy, const char *which)
6315 PurplePresence *presence;
6316 PurpleStatus *on, *off;
6318 presence = purple_buddy_get_presence(buddy);
6319 if (!presence)
6320 return;
6321 off = purple_presence_get_status(presence, "offline");
6322 on = purple_presence_get_status(presence, "available");
6324 if (*(which+1) == 'f')
6325 update_buddy_status_changed(buddy, on, off);
6326 else
6327 update_buddy_status_changed(buddy, off, on);
6330 static void
6331 update_conversation_switched(PurpleConversation *conv)
6333 pidgin_conv_update_fields(conv, PIDGIN_CONV_TAB_ICON |
6334 PIDGIN_CONV_SET_TITLE | PIDGIN_CONV_MENU |
6335 PIDGIN_CONV_BUDDY_ICON | PIDGIN_CONV_E2EE );
6338 static void
6339 update_buddy_typing(PurpleAccount *account, const char *who)
6341 PurpleConversation *conv;
6342 PidginConversation *gtkconv;
6344 conv = PURPLE_CONVERSATION(purple_conversations_find_im_with_account(who, account));
6345 if (!conv)
6346 return;
6348 gtkconv = PIDGIN_CONVERSATION(conv);
6349 if (gtkconv && gtkconv->active_conv == conv)
6350 pidgin_conv_update_fields(conv, PIDGIN_CONV_COLORIZE_TITLE);
6353 static void
6354 update_chat(PurpleChatConversation *chat)
6356 pidgin_conv_update_fields(PURPLE_CONVERSATION(chat), PIDGIN_CONV_TOPIC |
6357 PIDGIN_CONV_MENU | PIDGIN_CONV_SET_TITLE);
6360 static void
6361 update_chat_topic(PurpleChatConversation *chat, const char *old, const char *new)
6363 pidgin_conv_update_fields(PURPLE_CONVERSATION(chat), PIDGIN_CONV_TOPIC);
6366 /* Message history stuff */
6368 /* Compare two PurpleMessages, according to time in ascending order. */
6369 static int
6370 message_compare(PurpleMessage *m1, PurpleMessage *m2)
6372 guint64 t1 = purple_message_get_time(m1), t2 = purple_message_get_time(m2);
6373 return (t1 > t2) - (t1 < t2);
6376 /* Adds some message history to the gtkconv. This happens in a idle-callback. */
6377 static gboolean
6378 add_message_history_to_gtkconv(gpointer data)
6380 PidginConversation *gtkconv = data;
6381 int count = 0;
6382 int timer = gtkconv->attach_timer;
6383 time_t when = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(gtkconv->editor), "attach-start-time"));
6384 gboolean im = (PURPLE_IS_IM_CONVERSATION(gtkconv->active_conv));
6386 gtkconv->attach_timer = 0;
6387 while (gtkconv->attach_current && count < ADD_MESSAGE_HISTORY_AT_ONCE) {
6388 PurpleMessage *msg = gtkconv->attach_current->data;
6389 if (!im && when && (guint64)when < purple_message_get_time(msg)) {
6390 g_object_set_data(G_OBJECT(gtkconv->editor), "attach-start-time", NULL);
6392 /* XXX: should it be gtkconv->active_conv? */
6393 pidgin_conv_write_conv(gtkconv->active_conv, msg);
6394 if (im) {
6395 gtkconv->attach_current = g_list_delete_link(gtkconv->attach_current, gtkconv->attach_current);
6396 } else {
6397 gtkconv->attach_current = gtkconv->attach_current->prev;
6399 count++;
6401 gtkconv->attach_timer = timer;
6402 if (gtkconv->attach_current)
6403 return TRUE;
6405 g_source_remove(gtkconv->attach_timer);
6406 gtkconv->attach_timer = 0;
6407 if (im) {
6408 /* Print any message that was sent while the old history was being added back. */
6409 GList *msgs = NULL;
6410 GList *iter = gtkconv->convs;
6411 for (; iter; iter = iter->next) {
6412 PurpleConversation *conv = iter->data;
6413 GList *history = purple_conversation_get_message_history(conv);
6414 for (; history; history = history->next) {
6415 PurpleMessage *msg = history->data;
6416 if (purple_message_get_time(msg) > (guint64)when)
6417 msgs = g_list_prepend(msgs, msg);
6420 msgs = g_list_sort(msgs, (GCompareFunc)message_compare);
6421 for (; msgs; msgs = g_list_delete_link(msgs, msgs)) {
6422 PurpleMessage *msg = msgs->data;
6423 /* XXX: see above - should it be active_conv? */
6424 pidgin_conv_write_conv(gtkconv->active_conv, msg);
6426 g_object_set_data(G_OBJECT(gtkconv->editor), "attach-start-time", NULL);
6429 g_object_set_data(G_OBJECT(gtkconv->editor), "attach-start-time", NULL);
6430 purple_signal_emit(pidgin_conversations_get_handle(),
6431 "conversation-displayed", gtkconv);
6432 return FALSE;
6435 static void
6436 pidgin_conv_attach(PurpleConversation *conv)
6438 int timer;
6439 g_object_set_data(G_OBJECT(conv), "unseen-count", NULL);
6440 g_object_set_data(G_OBJECT(conv), "unseen-state", NULL);
6441 purple_conversation_set_ui_ops(conv, pidgin_conversations_get_conv_ui_ops());
6442 if (!PIDGIN_CONVERSATION(conv))
6443 private_gtkconv_new(conv, FALSE);
6444 timer = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(conv), "close-timer"));
6445 if (timer) {
6446 g_source_remove(timer);
6447 g_object_set_data(G_OBJECT(conv), "close-timer", NULL);
6451 gboolean pidgin_conv_attach_to_conversation(PurpleConversation *conv)
6453 GList *list;
6454 PidginConversation *gtkconv;
6456 if (PIDGIN_IS_PIDGIN_CONVERSATION(conv)) {
6457 /* This is pretty much always the case now. */
6458 gtkconv = PIDGIN_CONVERSATION(conv);
6459 if (gtkconv->win != hidden_convwin)
6460 return FALSE;
6461 pidgin_conv_window_remove_gtkconv(hidden_convwin, gtkconv);
6462 pidgin_conv_placement_place(gtkconv);
6463 purple_signal_emit(pidgin_conversations_get_handle(),
6464 "conversation-displayed", gtkconv);
6465 list = gtkconv->convs;
6466 while (list) {
6467 pidgin_conv_attach(list->data);
6468 list = list->next;
6470 return TRUE;
6473 pidgin_conv_attach(conv);
6474 gtkconv = PIDGIN_CONVERSATION(conv);
6476 list = purple_conversation_get_message_history(conv);
6477 if (list) {
6478 if (PURPLE_IS_IM_CONVERSATION(conv)) {
6479 GList *convs;
6480 list = g_list_copy(list);
6481 for (convs = purple_conversations_get_ims(); convs; convs = convs->next)
6482 if (convs->data != conv &&
6483 pidgin_conv_find_gtkconv(convs->data) == gtkconv) {
6484 pidgin_conv_attach(convs->data);
6485 list = g_list_concat(list, g_list_copy(purple_conversation_get_message_history(convs->data)));
6487 list = g_list_sort(list, (GCompareFunc)message_compare);
6488 gtkconv->attach_current = list;
6489 list = g_list_last(list);
6490 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
6491 gtkconv->attach_current = g_list_last(list);
6494 g_object_set_data(G_OBJECT(gtkconv->editor), "attach-start-time",
6495 GINT_TO_POINTER(purple_message_get_time(list->data)));
6496 gtkconv->attach_timer = g_idle_add(add_message_history_to_gtkconv, gtkconv);
6497 } else {
6498 purple_signal_emit(pidgin_conversations_get_handle(),
6499 "conversation-displayed", gtkconv);
6502 if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
6503 GList *users;
6504 PurpleChatConversation *chat = PURPLE_CHAT_CONVERSATION(conv);
6505 pidgin_conv_update_fields(conv, PIDGIN_CONV_TOPIC);
6506 users = purple_chat_conversation_get_users(chat);
6507 pidgin_conv_chat_add_users(chat, users, TRUE);
6508 g_list_free(users);
6511 return TRUE;
6514 void *
6515 pidgin_conversations_get_handle(void)
6517 static int handle;
6519 return &handle;
6522 static void
6523 pidgin_conversations_pre_uninit(void);
6525 void
6526 pidgin_conversations_init(void)
6528 void *handle = pidgin_conversations_get_handle();
6529 void *blist_handle = purple_blist_get_handle();
6531 e2ee_stock = g_hash_table_new_full(g_str_hash, g_str_equal,
6532 g_free, g_object_unref);
6534 /* Conversations */
6535 purple_prefs_add_none(PIDGIN_PREFS_ROOT "/conversations");
6536 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/use_smooth_scrolling", TRUE);
6537 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/close_on_tabs", TRUE);
6538 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/send_bold", FALSE);
6539 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/send_italic", FALSE);
6540 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/send_underline", FALSE);
6541 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/send_strike", FALSE);
6542 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/spellcheck", TRUE);
6543 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/show_incoming_formatting", TRUE);
6544 /* TODO: it's about *remote* smileys, not local ones */
6545 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/resize_custom_smileys", TRUE);
6546 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/custom_smileys_size", 96);
6547 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/minimum_entry_lines", 2);
6549 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/show_formatting_toolbar", TRUE);
6551 purple_prefs_add_string(PIDGIN_PREFS_ROOT "/conversations/placement", "last");
6552 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/placement_number", 1);
6553 purple_prefs_add_string(PIDGIN_PREFS_ROOT "/conversations/bgcolor", "");
6554 purple_prefs_add_string(PIDGIN_PREFS_ROOT "/conversations/fgcolor", "");
6555 purple_prefs_add_string(PIDGIN_PREFS_ROOT "/conversations/font_face", "");
6556 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/font_size", 3);
6557 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/tabs", TRUE);
6558 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/tab_side", GTK_POS_TOP);
6559 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/scrollback_lines", 4000);
6561 #ifdef _WIN32
6562 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/use_theme_font", TRUE);
6563 purple_prefs_add_string(PIDGIN_PREFS_ROOT "/conversations/custom_font", "");
6564 #endif
6566 /* Conversations -> Chat */
6567 purple_prefs_add_none(PIDGIN_PREFS_ROOT "/conversations/chat");
6568 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/chat/entry_height", 54);
6569 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/chat/userlist_width", 80);
6570 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/chat/x", 0);
6571 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/chat/y", 0);
6572 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/chat/width", 340);
6573 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/chat/height", 390);
6575 /* Conversations -> IM */
6576 purple_prefs_add_none(PIDGIN_PREFS_ROOT "/conversations/im");
6577 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/im/x", 0);
6578 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/im/y", 0);
6579 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/im/width", 340);
6580 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/im/height", 390);
6582 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/im/animate_buddy_icons", TRUE);
6584 purple_prefs_add_int(PIDGIN_PREFS_ROOT "/conversations/im/entry_height", 54);
6585 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/im/show_buddy_icons", TRUE);
6587 purple_prefs_add_string(PIDGIN_PREFS_ROOT "/conversations/im/hide_new", "never");
6588 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/conversations/im/close_immediately", TRUE);
6590 #ifdef _WIN32
6591 purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/win32/minimize_new_convs", FALSE);
6592 #endif
6594 /* Connect callbacks. */
6595 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/close_on_tabs",
6596 close_on_tabs_pref_cb, NULL);
6597 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/show_formatting_toolbar",
6598 show_formatting_toolbar_pref_cb, NULL);
6599 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/spellcheck",
6600 spellcheck_pref_cb, NULL);
6601 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/tab_side",
6602 tab_side_pref_cb, NULL);
6604 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/tabs",
6605 conv_placement_usetabs_cb, NULL);
6607 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/placement",
6608 conv_placement_pref_cb, NULL);
6609 purple_prefs_trigger_callback(PIDGIN_PREFS_ROOT "/conversations/placement");
6611 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/minimum_entry_lines",
6612 minimum_entry_lines_pref_cb, NULL);
6614 /* IM callbacks */
6615 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/im/animate_buddy_icons",
6616 animate_buddy_icons_pref_cb, NULL);
6617 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/im/show_buddy_icons",
6618 show_buddy_icons_pref_cb, NULL);
6619 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/blist/show_protocol_icons",
6620 show_protocol_icons_pref_cb, NULL);
6621 purple_prefs_connect_callback(handle, PIDGIN_PREFS_ROOT "/conversations/im/hide_new",
6622 hide_new_pref_cb, NULL);
6624 /**********************************************************************
6625 * Register signals
6626 **********************************************************************/
6627 purple_signal_register(handle, "conversation-dragging",
6628 purple_marshal_VOID__POINTER_POINTER, G_TYPE_NONE, 2,
6629 G_TYPE_POINTER, /* pointer to a (PidginConvWindow *) */
6630 G_TYPE_POINTER); /* pointer to a (PidginConvWindow *) */
6632 purple_signal_register(handle, "conversation-timestamp",
6633 #if SIZEOF_TIME_T == 4
6634 purple_marshal_POINTER__POINTER_INT_BOOLEAN,
6635 #elif SIZEOF_TIME_T == 8
6636 purple_marshal_POINTER__POINTER_INT64_BOOLEAN,
6637 #else
6638 #error Unkown size of time_t
6639 #endif
6640 G_TYPE_STRING, 3, PURPLE_TYPE_CONVERSATION,
6641 #if SIZEOF_TIME_T == 4
6642 G_TYPE_INT,
6643 #elif SIZEOF_TIME_T == 8
6644 G_TYPE_INT64,
6645 #else
6646 # error Unknown size of time_t
6647 #endif
6648 G_TYPE_BOOLEAN);
6650 purple_signal_register(handle, "displaying-im-msg",
6651 purple_marshal_BOOLEAN__POINTER_POINTER,
6652 G_TYPE_BOOLEAN, 2, PURPLE_TYPE_CONVERSATION, PURPLE_TYPE_MESSAGE);
6654 purple_signal_register(handle, "displayed-im-msg",
6655 purple_marshal_VOID__POINTER_POINTER, G_TYPE_NONE, 2,
6656 PURPLE_TYPE_CONVERSATION, PURPLE_TYPE_MESSAGE);
6658 purple_signal_register(handle, "displaying-chat-msg",
6659 purple_marshal_BOOLEAN__POINTER_POINTER,
6660 G_TYPE_BOOLEAN, 2, PURPLE_TYPE_CONVERSATION, PURPLE_TYPE_MESSAGE);
6662 purple_signal_register(handle, "displayed-chat-msg",
6663 purple_marshal_VOID__POINTER_POINTER, G_TYPE_NONE, 2,
6664 PURPLE_TYPE_CONVERSATION, PURPLE_TYPE_MESSAGE);
6666 purple_signal_register(handle, "conversation-switched",
6667 purple_marshal_VOID__POINTER, G_TYPE_NONE, 1,
6668 PURPLE_TYPE_CONVERSATION);
6670 purple_signal_register(handle, "conversation-hiding",
6671 purple_marshal_VOID__POINTER, G_TYPE_NONE, 1,
6672 G_TYPE_POINTER); /* (PidginConversation *) */
6674 purple_signal_register(handle, "conversation-displayed",
6675 purple_marshal_VOID__POINTER, G_TYPE_NONE, 1,
6676 G_TYPE_POINTER); /* (PidginConversation *) */
6678 purple_signal_register(handle, "chat-nick-autocomplete",
6679 purple_marshal_BOOLEAN__POINTER_BOOLEAN,
6680 G_TYPE_BOOLEAN, 1, PURPLE_TYPE_CONVERSATION);
6682 purple_signal_register(handle, "chat-nick-clicked",
6683 purple_marshal_BOOLEAN__POINTER_POINTER_UINT,
6684 G_TYPE_BOOLEAN, 3, PURPLE_TYPE_CONVERSATION,
6685 G_TYPE_STRING, G_TYPE_UINT);
6687 purple_signal_register(handle, "conversation-window-created",
6688 purple_marshal_VOID__POINTER, G_TYPE_NONE, 1,
6689 G_TYPE_POINTER); /* (PidginConvWindow *) */
6692 /**********************************************************************
6693 * Register commands
6694 **********************************************************************/
6695 purple_cmd_register("say", "S", PURPLE_CMD_P_DEFAULT,
6696 PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM, NULL,
6697 say_command_cb, _("say &lt;message&gt;: Send a message normally as if you weren't using a command."), NULL);
6698 purple_cmd_register("me", "S", PURPLE_CMD_P_DEFAULT,
6699 PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM, NULL,
6700 me_command_cb, _("me &lt;action&gt;: Send an IRC style action to a buddy or chat."), NULL);
6701 purple_cmd_register("debug", "w", PURPLE_CMD_P_DEFAULT,
6702 PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM, NULL,
6703 debug_command_cb, _("debug &lt;option&gt;: Send various debug information to the current conversation."), NULL);
6704 purple_cmd_register("clear", "", PURPLE_CMD_P_DEFAULT,
6705 PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM, NULL,
6706 clear_command_cb, _("clear: Clears the conversation scrollback."), NULL);
6707 purple_cmd_register("clearall", "", PURPLE_CMD_P_DEFAULT,
6708 PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM, NULL,
6709 clearall_command_cb, _("clear: Clears all conversation scrollbacks."), NULL);
6710 purple_cmd_register("help", "w", PURPLE_CMD_P_DEFAULT,
6711 PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM | PURPLE_CMD_FLAG_ALLOW_WRONG_ARGS, NULL,
6712 help_command_cb, _("help &lt;command&gt;: Help on a specific command."), NULL);
6714 /**********************************************************************
6715 * UI operations
6716 **********************************************************************/
6718 purple_signal_connect(purple_connections_get_handle(), "signed-on", handle,
6719 G_CALLBACK(account_signed_off_cb),
6720 GINT_TO_POINTER(PURPLE_CONVERSATION_ACCOUNT_ONLINE));
6721 purple_signal_connect(purple_connections_get_handle(), "signed-off", handle,
6722 G_CALLBACK(account_signed_off_cb),
6723 GINT_TO_POINTER(PURPLE_CONVERSATION_ACCOUNT_OFFLINE));
6724 purple_signal_connect(purple_connections_get_handle(), "signing-off", handle,
6725 G_CALLBACK(account_signing_off), NULL);
6727 purple_signal_connect(purple_conversations_get_handle(), "writing-im-msg",
6728 handle, G_CALLBACK(writing_msg), NULL);
6729 purple_signal_connect(purple_conversations_get_handle(), "writing-chat-msg",
6730 handle, G_CALLBACK(writing_msg), NULL);
6731 purple_signal_connect(purple_conversations_get_handle(), "received-im-msg",
6732 handle, G_CALLBACK(received_im_msg_cb), NULL);
6733 purple_signal_connect(purple_conversations_get_handle(), "cleared-message-history",
6734 handle, G_CALLBACK(clear_conversation_scrollback_cb), NULL);
6736 purple_signal_connect(purple_conversations_get_handle(), "deleting-chat-user",
6737 handle, G_CALLBACK(deleting_chat_user_cb), NULL);
6739 purple_conversations_set_ui_ops(&conversation_ui_ops);
6741 hidden_convwin = pidgin_conv_window_new();
6742 window_list = g_list_remove(window_list, hidden_convwin);
6744 purple_signal_connect(purple_accounts_get_handle(), "account-status-changed",
6745 handle, PURPLE_CALLBACK(account_status_changed_cb), NULL);
6747 purple_signal_connect_priority(purple_get_core(), "quitting", handle,
6748 PURPLE_CALLBACK(pidgin_conversations_pre_uninit), NULL, PURPLE_SIGNAL_PRIORITY_HIGHEST);
6750 /* Callbacks to update a conversation */
6751 purple_signal_connect(blist_handle, "blist-node-added", handle,
6752 G_CALLBACK(buddy_update_cb), NULL);
6753 purple_signal_connect(blist_handle, "blist-node-removed", handle,
6754 G_CALLBACK(buddy_update_cb), NULL);
6755 purple_signal_connect(blist_handle, "buddy-signed-on",
6756 handle, PURPLE_CALLBACK(update_buddy_sign), "on");
6757 purple_signal_connect(blist_handle, "buddy-signed-off",
6758 handle, PURPLE_CALLBACK(update_buddy_sign), "off");
6759 purple_signal_connect(blist_handle, "buddy-status-changed",
6760 handle, PURPLE_CALLBACK(update_buddy_status_changed), NULL);
6761 purple_signal_connect(blist_handle, "buddy-privacy-changed",
6762 handle, PURPLE_CALLBACK(update_buddy_privacy_changed), NULL);
6763 purple_signal_connect(blist_handle, "buddy-idle-changed",
6764 handle, PURPLE_CALLBACK(update_buddy_idle_changed), NULL);
6765 purple_signal_connect(blist_handle, "buddy-icon-changed",
6766 handle, PURPLE_CALLBACK(update_buddy_icon), NULL);
6767 purple_signal_connect(purple_conversations_get_handle(), "buddy-typing",
6768 handle, PURPLE_CALLBACK(update_buddy_typing), NULL);
6769 purple_signal_connect(purple_conversations_get_handle(), "buddy-typing-stopped",
6770 handle, PURPLE_CALLBACK(update_buddy_typing), NULL);
6771 purple_signal_connect(pidgin_conversations_get_handle(), "conversation-switched",
6772 handle, PURPLE_CALLBACK(update_conversation_switched), NULL);
6773 purple_signal_connect(purple_conversations_get_handle(), "chat-left", handle,
6774 PURPLE_CALLBACK(update_chat), NULL);
6775 purple_signal_connect(purple_conversations_get_handle(), "chat-joined", handle,
6776 PURPLE_CALLBACK(update_chat), NULL);
6777 purple_signal_connect(purple_conversations_get_handle(), "chat-topic-changed", handle,
6778 PURPLE_CALLBACK(update_chat_topic), NULL);
6779 purple_signal_connect_priority(purple_conversations_get_handle(), "conversation-updated", handle,
6780 PURPLE_CALLBACK(pidgin_conv_updated), NULL,
6781 PURPLE_SIGNAL_PRIORITY_LOWEST);
6782 purple_signal_connect(purple_conversations_get_handle(), "wrote-im-msg", handle,
6783 PURPLE_CALLBACK(wrote_msg_update_unseen_cb), NULL);
6784 purple_signal_connect(purple_conversations_get_handle(), "wrote-chat-msg", handle,
6785 PURPLE_CALLBACK(wrote_msg_update_unseen_cb), NULL);
6788 static void
6789 pidgin_conversations_pre_uninit(void)
6791 g_hash_table_destroy(e2ee_stock);
6792 e2ee_stock = NULL;
6795 /* Invalidate the first tab color set */
6796 static gboolean tab_color_fuse = TRUE;
6798 static void
6799 pidgin_conversations_set_tab_colors(void)
6801 /* Set default tab colors */
6802 GString *str = g_string_new(NULL);
6803 GtkSettings *settings = gtk_settings_get_default();
6804 GtkStyle *parent = gtk_rc_get_style_by_paths(settings, "tab-container.tab-label*", NULL, G_TYPE_NONE), *now;
6805 struct {
6806 const char *stylename;
6807 const char *labelname;
6808 const char *color;
6809 } styles[] = {
6810 {"pidgin_tab_label_typing_default", "tab-label-typing", "#4e9a06"},
6811 {"pidgin_tab_label_typed_default", "tab-label-typed", "#c4a000"},
6812 {"pidgin_tab_label_attention_default", "tab-label-attention", "#006aff"},
6813 {"pidgin_tab_label_unreadchat_default", "tab-label-unreadchat", "#cc0000"},
6814 {"pidgin_tab_label_event_default", "tab-label-event", "#888a85"},
6815 {NULL, NULL, NULL}
6817 int iter;
6819 if(tab_color_fuse) {
6820 tab_color_fuse = FALSE;
6821 return;
6824 for (iter = 0; styles[iter].stylename; iter++) {
6825 now = gtk_rc_get_style_by_paths(settings, styles[iter].labelname, NULL, G_TYPE_NONE);
6826 if (parent == now ||
6827 (parent && now && parent->rc_style == now->rc_style)) {
6828 GdkRGBA color;
6829 gchar *color_str;
6831 gdk_rgba_parse(&color, styles[iter].color);
6832 pidgin_style_adjust_contrast(gtk_widget_get_default_style(), &color);
6834 color_str = gdk_rgba_to_string(&color);
6835 g_string_append_printf(str, "style \"%s\" {\n"
6836 "fg[ACTIVE] = \"%s\"\n"
6837 "}\n"
6838 "widget \"*%s\" style \"%s\"\n",
6839 styles[iter].stylename,
6840 color_str,
6841 styles[iter].labelname, styles[iter].stylename);
6842 g_free(color_str);
6845 gtk_rc_parse_string(str->str);
6846 g_string_free(str, TRUE);
6847 gtk_rc_reset_styles(settings);
6850 void
6851 pidgin_conversations_uninit(void)
6853 purple_prefs_disconnect_by_handle(pidgin_conversations_get_handle());
6854 purple_signals_disconnect_by_handle(pidgin_conversations_get_handle());
6855 purple_signals_unregister_by_instance(pidgin_conversations_get_handle());
6858 /**************************************************************************
6859 * PidginConversation GBoxed code
6860 **************************************************************************/
6861 static PidginConversation *
6862 pidgin_conversation_ref(PidginConversation *gtkconv)
6864 g_return_val_if_fail(gtkconv != NULL, NULL);
6866 gtkconv->box_count++;
6868 return gtkconv;
6871 static void
6872 pidgin_conversation_unref(PidginConversation *gtkconv)
6874 g_return_if_fail(gtkconv != NULL);
6875 g_return_if_fail(gtkconv->box_count >= 0);
6877 if (!gtkconv->box_count--)
6878 pidgin_conv_destroy(gtkconv->active_conv);
6881 GType
6882 pidgin_conversation_get_type(void)
6884 static GType type = 0;
6886 if (type == 0) {
6887 type = g_boxed_type_register_static("PidginConversation",
6888 (GBoxedCopyFunc)pidgin_conversation_ref,
6889 (GBoxedFreeFunc)pidgin_conversation_unref);
6892 return type;
6910 /* down here is where gtkconvwin.c ought to start. except they share like every freaking function,
6911 * and touch each others' private members all day long */
6913 /* pidgin
6915 * Pidgin is the legal property of its developers, whose names are too numerous
6916 * to list here. Please refer to the COPYRIGHT file distributed with this
6917 * source distribution.
6919 * This program is free software; you can redistribute it and/or modify
6920 * it under the terms of the GNU General Public License as published by
6921 * the Free Software Foundation; either version 2 of the License, or
6922 * (at your option) any later version.
6924 * This program is distributed in the hope that it will be useful,
6925 * but WITHOUT ANY WARRANTY; without even the implied warranty of
6926 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
6927 * GNU General Public License for more details.
6929 * You should have received a copy of the GNU General Public License
6930 * along with this program; if not, write to the Free Software
6931 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA
6934 #include "internal.h"
6935 #include "pidgin.h"
6938 #include <gdk/gdkkeysyms.h>
6940 #include "account.h"
6941 #include "cmds.h"
6942 #include "debug.h"
6943 #include "log.h"
6944 #include "notify.h"
6945 #include "protocol.h"
6946 #include "request.h"
6947 #include "util.h"
6949 #include "gtkdnd-hints.h"
6950 #include "gtkblist.h"
6951 #include "gtkconv.h"
6952 #include "gtkdialogs.h"
6953 #include "gtkmenutray.h"
6954 #include "gtkpounce.h"
6955 #include "gtkprefs.h"
6956 #include "gtkprivacy.h"
6957 #include "gtkutils.h"
6958 #include "pidginstock.h"
6960 static void
6961 do_close(GtkWidget *w, int resp, PidginConvWindow *win)
6963 gtk_widget_destroy(warn_close_dialog);
6964 warn_close_dialog = NULL;
6966 if (resp == GTK_RESPONSE_OK)
6967 pidgin_conv_window_destroy(win);
6970 static void
6971 build_warn_close_dialog(PidginConvWindow *gtkwin)
6973 GtkWidget *label, *vbox, *hbox, *img;
6975 g_return_if_fail(warn_close_dialog == NULL);
6977 warn_close_dialog = gtk_dialog_new_with_buttons(_("Confirm close"),
6978 GTK_WINDOW(gtkwin->window), GTK_DIALOG_MODAL,
6979 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
6980 GTK_STOCK_CLOSE, GTK_RESPONSE_OK, NULL);
6982 gtk_dialog_set_default_response(GTK_DIALOG(warn_close_dialog),
6983 GTK_RESPONSE_OK);
6985 gtk_container_set_border_width(GTK_CONTAINER(warn_close_dialog),
6987 gtk_window_set_resizable(GTK_WINDOW(warn_close_dialog), FALSE);
6989 /* Setup the outside spacing. */
6990 vbox = gtk_dialog_get_content_area(GTK_DIALOG(warn_close_dialog));
6992 gtk_box_set_spacing(GTK_BOX(vbox), 12);
6993 gtk_container_set_border_width(GTK_CONTAINER(vbox), 6);
6995 img = gtk_image_new_from_icon_name("dialog-warning",
6996 GTK_ICON_SIZE_DIALOG);
6998 /* Setup the inner hbox and put the dialog's icon in it. */
6999 hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 12);
7000 gtk_container_add(GTK_CONTAINER(vbox), hbox);
7001 gtk_box_pack_start(GTK_BOX(hbox), img, FALSE, FALSE, 0);
7002 gtk_widget_set_halign(img, GTK_ALIGN_START);
7003 gtk_widget_set_valign(img, GTK_ALIGN_START);
7005 /* Setup the right vbox. */
7006 vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12);
7007 gtk_container_add(GTK_CONTAINER(hbox), vbox);
7009 label = gtk_label_new(_("You have unread messages. Are you sure you want to close the window?"));
7010 gtk_widget_set_size_request(label, 350, -1);
7011 gtk_label_set_line_wrap(GTK_LABEL(label), TRUE);
7012 gtk_label_set_xalign(GTK_LABEL(label), 0);
7013 gtk_label_set_yalign(GTK_LABEL(label), 0);
7014 gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, FALSE, 0);
7016 /* Connect the signals. */
7017 g_signal_connect(G_OBJECT(warn_close_dialog), "response",
7018 G_CALLBACK(do_close), gtkwin);
7022 /**************************************************************************
7023 * Callbacks
7024 **************************************************************************/
7026 static gboolean
7027 close_win_cb(GtkWidget *w, GdkEventAny *e, gpointer d)
7029 PidginConvWindow *win = d;
7030 GList *l;
7032 /* If there are unread messages then show a warning dialog */
7033 for (l = pidgin_conv_window_get_gtkconvs(win);
7034 l != NULL; l = l->next)
7036 PidginConversation *gtkconv = l->data;
7037 if (PURPLE_IS_IM_CONVERSATION(gtkconv->active_conv) &&
7038 gtkconv->unseen_state >= PIDGIN_UNSEEN_TEXT)
7040 build_warn_close_dialog(win);
7041 gtk_widget_show_all(warn_close_dialog);
7043 return TRUE;
7047 pidgin_conv_window_destroy(win);
7049 return TRUE;
7052 static void
7053 conv_set_unseen(PurpleConversation *conv, PidginUnseenState state)
7055 int unseen_count = 0;
7056 PidginUnseenState unseen_state = PIDGIN_UNSEEN_NONE;
7058 if(g_object_get_data(G_OBJECT(conv), "unseen-count"))
7059 unseen_count = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(conv), "unseen-count"));
7061 if(g_object_get_data(G_OBJECT(conv), "unseen-state"))
7062 unseen_state = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(conv), "unseen-state"));
7064 if (state == PIDGIN_UNSEEN_NONE)
7066 unseen_count = 0;
7067 unseen_state = PIDGIN_UNSEEN_NONE;
7069 else
7071 if (state >= PIDGIN_UNSEEN_TEXT)
7072 unseen_count++;
7074 if (state > unseen_state)
7075 unseen_state = state;
7078 g_object_set_data(G_OBJECT(conv), "unseen-count", GINT_TO_POINTER(unseen_count));
7079 g_object_set_data(G_OBJECT(conv), "unseen-state", GINT_TO_POINTER(unseen_state));
7081 purple_conversation_update(conv, PURPLE_CONVERSATION_UPDATE_UNSEEN);
7084 static void
7085 gtkconv_set_unseen(PidginConversation *gtkconv, PidginUnseenState state)
7087 if (state == PIDGIN_UNSEEN_NONE)
7089 gtkconv->unseen_count = 0;
7090 gtkconv->unseen_state = PIDGIN_UNSEEN_NONE;
7092 else
7094 if (state >= PIDGIN_UNSEEN_TEXT)
7095 gtkconv->unseen_count++;
7097 if (state > gtkconv->unseen_state)
7098 gtkconv->unseen_state = state;
7101 g_object_set_data(G_OBJECT(gtkconv->active_conv), "unseen-count", GINT_TO_POINTER(gtkconv->unseen_count));
7102 g_object_set_data(G_OBJECT(gtkconv->active_conv), "unseen-state", GINT_TO_POINTER(gtkconv->unseen_state));
7104 purple_conversation_update(gtkconv->active_conv, PURPLE_CONVERSATION_UPDATE_UNSEEN);
7108 * When a conversation window is focused, we know the user
7109 * has looked at it so we know there are no longer unseen
7110 * messages.
7112 static gboolean
7113 focus_win_cb(GtkWidget *w, GdkEventFocus *e, gpointer d)
7115 PidginConvWindow *win = d;
7116 PidginConversation *gtkconv = pidgin_conv_window_get_active_gtkconv(win);
7118 if (gtkconv)
7119 gtkconv_set_unseen(gtkconv, PIDGIN_UNSEEN_NONE);
7121 return FALSE;
7124 static void
7125 notebook_init_grab(PidginConvWindow *gtkwin, GtkWidget *widget, GdkEvent *event)
7127 static GdkCursor *cursor = NULL;
7128 GdkDevice *device;
7130 gtkwin->in_drag = TRUE;
7132 if (gtkwin->drag_leave_signal) {
7133 g_signal_handler_disconnect(G_OBJECT(widget),
7134 gtkwin->drag_leave_signal);
7135 gtkwin->drag_leave_signal = 0;
7138 if (cursor == NULL) {
7139 GdkDisplay *display = gtk_widget_get_display(gtkwin->notebook);
7140 cursor = gdk_cursor_new_for_display(display, GDK_FLEUR);
7143 /* Grab the pointer */
7144 gtk_grab_add(gtkwin->notebook);
7145 device = gdk_event_get_device(event);
7146 if (!gdk_display_device_is_grabbed(gdk_device_get_display(device), device))
7147 gdk_device_grab(device, gtk_widget_get_window(gtkwin->notebook),
7148 GDK_OWNERSHIP_WINDOW, FALSE,
7149 GDK_BUTTON1_MOTION_MASK | GDK_BUTTON_RELEASE_MASK,
7150 cursor, gdk_event_get_time(event));
7153 static gboolean
7154 notebook_motion_cb(GtkWidget *widget, GdkEventButton *e, PidginConvWindow *win)
7158 * Make sure the user moved the mouse far enough for the
7159 * drag to be initiated.
7161 if (win->in_predrag) {
7162 if (e->x_root < win->drag_min_x ||
7163 e->x_root >= win->drag_max_x ||
7164 e->y_root < win->drag_min_y ||
7165 e->y_root >= win->drag_max_y) {
7167 win->in_predrag = FALSE;
7168 notebook_init_grab(win, widget, (GdkEvent *)e);
7171 else { /* Otherwise, draw the arrows. */
7172 PidginConvWindow *dest_win;
7173 GtkNotebook *dest_notebook;
7174 GtkWidget *tab;
7175 gint page_num;
7176 gboolean horiz_tabs = FALSE;
7177 gboolean to_right = FALSE;
7179 /* Get the window that the cursor is over. */
7180 dest_win = pidgin_conv_window_get_at_event((GdkEvent *)e);
7182 if (dest_win == NULL) {
7183 pidgin_dnd_hints_hide_all();
7185 return TRUE;
7188 dest_notebook = GTK_NOTEBOOK(dest_win->notebook);
7190 if (gtk_notebook_get_show_tabs(dest_notebook)) {
7191 page_num = pidgin_conv_get_tab_at_xy(dest_win,
7192 e->x_root, e->y_root, &to_right);
7193 to_right = to_right && (win != dest_win);
7194 tab = pidgin_conv_window_get_gtkconv_at_index(dest_win, page_num)->tabby;
7195 } else {
7196 page_num = 0;
7197 to_right = pidgin_conv_xy_to_right_infopane(dest_win, e->x_root, e->y_root);
7198 tab = pidgin_conv_window_get_gtkconv_at_index(dest_win, page_num)->infopane_hbox;
7201 if (gtk_notebook_get_tab_pos(dest_notebook) == GTK_POS_TOP ||
7202 gtk_notebook_get_tab_pos(dest_notebook) == GTK_POS_BOTTOM) {
7203 horiz_tabs = TRUE;
7206 if (gtk_notebook_get_show_tabs(dest_notebook) == FALSE && win == dest_win)
7208 /* dragging a tab from a single-tabbed window over its own window */
7209 pidgin_dnd_hints_hide_all();
7210 return TRUE;
7211 } else if (horiz_tabs) {
7212 if (((gpointer)win == (gpointer)dest_win && win->drag_tab < page_num) || to_right) {
7213 pidgin_dnd_hints_show_relative(HINT_ARROW_DOWN, tab, HINT_POSITION_RIGHT, HINT_POSITION_TOP);
7214 pidgin_dnd_hints_show_relative(HINT_ARROW_UP, tab, HINT_POSITION_RIGHT, HINT_POSITION_BOTTOM);
7215 } else {
7216 pidgin_dnd_hints_show_relative(HINT_ARROW_DOWN, tab, HINT_POSITION_LEFT, HINT_POSITION_TOP);
7217 pidgin_dnd_hints_show_relative(HINT_ARROW_UP, tab, HINT_POSITION_LEFT, HINT_POSITION_BOTTOM);
7219 } else {
7220 if (((gpointer)win == (gpointer)dest_win && win->drag_tab < page_num) || to_right) {
7221 pidgin_dnd_hints_show_relative(HINT_ARROW_RIGHT, tab, HINT_POSITION_LEFT, HINT_POSITION_BOTTOM);
7222 pidgin_dnd_hints_show_relative(HINT_ARROW_LEFT, tab, HINT_POSITION_RIGHT, HINT_POSITION_BOTTOM);
7223 } else {
7224 pidgin_dnd_hints_show_relative(HINT_ARROW_RIGHT, tab, HINT_POSITION_LEFT, HINT_POSITION_TOP);
7225 pidgin_dnd_hints_show_relative(HINT_ARROW_LEFT, tab, HINT_POSITION_RIGHT, HINT_POSITION_TOP);
7230 return TRUE;
7233 static gboolean
7234 notebook_leave_cb(GtkWidget *widget, GdkEventCrossing *e, PidginConvWindow *win)
7236 if (win->in_drag)
7237 return FALSE;
7239 if (e->x_root < win->drag_min_x ||
7240 e->x_root >= win->drag_max_x ||
7241 e->y_root < win->drag_min_y ||
7242 e->y_root >= win->drag_max_y) {
7244 win->in_predrag = FALSE;
7245 notebook_init_grab(win, widget, (GdkEvent *)e);
7248 return TRUE;
7252 * THANK YOU GALEON!
7255 static gboolean
7256 infopane_press_cb(GtkWidget *widget, GdkEventButton *e, PidginConversation *gtkconv)
7258 if (e->type == GDK_2BUTTON_PRESS && e->button == GDK_BUTTON_PRIMARY) {
7259 if (infopane_entry_activate(gtkconv))
7260 return TRUE;
7263 if (e->type != GDK_BUTTON_PRESS)
7264 return FALSE;
7266 if (e->button == GDK_BUTTON_PRIMARY) {
7267 int nb_x, nb_y;
7268 GtkAllocation allocation;
7270 gtk_widget_get_allocation(gtkconv->infopane_hbox, &allocation);
7272 if (gtkconv->win->in_drag)
7273 return TRUE;
7275 gtkconv->win->in_predrag = TRUE;
7276 gtkconv->win->drag_tab = gtk_notebook_page_num(GTK_NOTEBOOK(gtkconv->win->notebook), gtkconv->tab_cont);
7278 gdk_window_get_origin(gtk_widget_get_window(gtkconv->infopane_hbox), &nb_x, &nb_y);
7280 gtkconv->win->drag_min_x = allocation.x + nb_x;
7281 gtkconv->win->drag_min_y = allocation.y + nb_y;
7282 gtkconv->win->drag_max_x = allocation.width + gtkconv->win->drag_min_x;
7283 gtkconv->win->drag_max_y = allocation.height + gtkconv->win->drag_min_y;
7285 gtkconv->win->drag_motion_signal = g_signal_connect(G_OBJECT(gtkconv->win->notebook), "motion_notify_event",
7286 G_CALLBACK(notebook_motion_cb), gtkconv->win);
7287 gtkconv->win->drag_leave_signal = g_signal_connect(G_OBJECT(gtkconv->win->notebook), "leave_notify_event",
7288 G_CALLBACK(notebook_leave_cb), gtkconv->win);
7289 return FALSE;
7292 if (gdk_event_triggers_context_menu((GdkEvent *)e)) {
7293 /* Right click was pressed. Popup the context menu. */
7294 GtkWidget *menu = gtk_menu_new(), *sub;
7295 gboolean populated = populate_menu_with_options(menu, gtkconv, TRUE);
7297 sub = gtk_menu_item_get_submenu(GTK_MENU_ITEM(gtkconv->win->menu->send_to));
7298 if (sub && gtk_widget_is_sensitive(gtkconv->win->menu->send_to)) {
7299 GtkWidget *item = gtk_menu_item_new_with_mnemonic(_("S_end To"));
7300 if (populated)
7301 pidgin_separator(menu);
7302 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
7303 gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), sub);
7304 gtk_widget_show(item);
7305 gtk_widget_show_all(sub);
7306 } else if (!populated) {
7307 gtk_widget_destroy(menu);
7308 return FALSE;
7311 gtk_widget_show_all(menu);
7312 gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent *)e);
7313 return TRUE;
7315 return FALSE;
7318 static gboolean
7319 notebook_press_cb(GtkWidget *widget, GdkEventButton *e, PidginConvWindow *win)
7321 gint nb_x, nb_y;
7322 int tab_clicked;
7323 GtkWidget *page;
7324 GtkWidget *tab;
7325 GtkAllocation allocation;
7327 if (e->button == GDK_BUTTON_MIDDLE && e->type == GDK_BUTTON_PRESS) {
7328 PidginConversation *gtkconv;
7329 tab_clicked = pidgin_conv_get_tab_at_xy(win, e->x_root, e->y_root, NULL);
7331 if (tab_clicked == -1)
7332 return FALSE;
7334 gtkconv = pidgin_conv_window_get_gtkconv_at_index(win, tab_clicked);
7335 close_conv_cb(NULL, gtkconv);
7336 return TRUE;
7340 if (e->button != GDK_BUTTON_PRIMARY || e->type != GDK_BUTTON_PRESS)
7341 return FALSE;
7344 if (win->in_drag) {
7345 purple_debug(PURPLE_DEBUG_WARNING, "gtkconv",
7346 "Already in the middle of a window drag at tab_press_cb\n");
7347 return TRUE;
7351 * Make sure a tab was actually clicked. The arrow buttons
7352 * mess things up.
7354 tab_clicked = pidgin_conv_get_tab_at_xy(win, e->x_root, e->y_root, NULL);
7356 if (tab_clicked == -1)
7357 return FALSE;
7360 * Get the relative position of the press event, with regards to
7361 * the position of the notebook.
7363 gdk_window_get_origin(gtk_widget_get_window(win->notebook), &nb_x, &nb_y);
7365 /* Reset the min/max x/y */
7366 win->drag_min_x = 0;
7367 win->drag_min_y = 0;
7368 win->drag_max_x = 0;
7369 win->drag_max_y = 0;
7371 /* Find out which tab was dragged. */
7372 page = gtk_notebook_get_nth_page(GTK_NOTEBOOK(win->notebook), tab_clicked);
7373 tab = gtk_notebook_get_tab_label(GTK_NOTEBOOK(win->notebook), page);
7375 gtk_widget_get_allocation(tab, &allocation);
7377 win->drag_min_x = allocation.x + nb_x;
7378 win->drag_min_y = allocation.y + nb_y;
7379 win->drag_max_x = allocation.width + win->drag_min_x;
7380 win->drag_max_y = allocation.height + win->drag_min_y;
7382 /* Make sure the click occurred in the tab. */
7383 if (e->x_root < win->drag_min_x ||
7384 e->x_root >= win->drag_max_x ||
7385 e->y_root < win->drag_min_y ||
7386 e->y_root >= win->drag_max_y) {
7388 return FALSE;
7391 win->in_predrag = TRUE;
7392 win->drag_tab = tab_clicked;
7394 /* Connect the new motion signals. */
7395 win->drag_motion_signal =
7396 g_signal_connect(G_OBJECT(widget), "motion_notify_event",
7397 G_CALLBACK(notebook_motion_cb), win);
7399 win->drag_leave_signal =
7400 g_signal_connect(G_OBJECT(widget), "leave_notify_event",
7401 G_CALLBACK(notebook_leave_cb), win);
7403 return FALSE;
7406 static gboolean
7407 notebook_release_cb(GtkWidget *widget, GdkEventButton *e, PidginConvWindow *win)
7409 PidginConvWindow *dest_win;
7410 GtkNotebook *dest_notebook;
7411 PidginConversation *active_gtkconv;
7412 PidginConversation *gtkconv;
7413 gint dest_page_num = 0;
7414 gboolean new_window = FALSE;
7415 gboolean to_right = FALSE;
7416 GdkDevice *device;
7419 * Don't check to make sure that the event's window matches the
7420 * widget's, because we may be getting an event passed on from the
7421 * close button.
7423 if (e->button != GDK_BUTTON_PRIMARY && e->type != GDK_BUTTON_RELEASE)
7424 return FALSE;
7426 device = gdk_event_get_device((GdkEvent *)e);
7427 if (gdk_display_device_is_grabbed(gdk_device_get_display(device), device)) {
7428 gdk_device_ungrab(device, gdk_event_get_time((GdkEvent *)e));
7429 gtk_grab_remove(widget);
7432 if (!win->in_predrag && !win->in_drag)
7433 return FALSE;
7435 /* Disconnect the motion signal. */
7436 if (win->drag_motion_signal) {
7437 g_signal_handler_disconnect(G_OBJECT(widget),
7438 win->drag_motion_signal);
7440 win->drag_motion_signal = 0;
7444 * If we're in a pre-drag, we'll also need to disconnect the leave
7445 * signal.
7447 if (win->in_predrag) {
7448 win->in_predrag = FALSE;
7450 if (win->drag_leave_signal) {
7451 g_signal_handler_disconnect(G_OBJECT(widget),
7452 win->drag_leave_signal);
7454 win->drag_leave_signal = 0;
7458 /* If we're not in drag... */
7459 /* We're perfectly normal people! */
7460 if (!win->in_drag)
7461 return FALSE;
7463 win->in_drag = FALSE;
7465 pidgin_dnd_hints_hide_all();
7467 dest_win = pidgin_conv_window_get_at_event((GdkEvent *)e);
7469 active_gtkconv = pidgin_conv_window_get_active_gtkconv(win);
7471 if (dest_win == NULL) {
7472 /* If the current window doesn't have any other conversations,
7473 * there isn't much point transferring the conv to a new window. */
7474 if (pidgin_conv_window_get_gtkconv_count(win) > 1) {
7475 /* Make a new window to stick this to. */
7476 dest_win = pidgin_conv_window_new();
7477 new_window = TRUE;
7481 if (dest_win == NULL)
7482 return FALSE;
7484 purple_signal_emit(pidgin_conversations_get_handle(),
7485 "conversation-dragging", win, dest_win);
7487 /* Get the destination page number. */
7488 if (!new_window) {
7489 dest_notebook = GTK_NOTEBOOK(dest_win->notebook);
7490 if (gtk_notebook_get_show_tabs(dest_notebook)) {
7491 dest_page_num = pidgin_conv_get_tab_at_xy(dest_win,
7492 e->x_root, e->y_root, &to_right);
7493 } else {
7494 dest_page_num = 0;
7495 to_right = pidgin_conv_xy_to_right_infopane(dest_win, e->x_root, e->y_root);
7499 gtkconv = pidgin_conv_window_get_gtkconv_at_index(win, win->drag_tab);
7501 if (win == dest_win) {
7502 gtk_notebook_reorder_child(GTK_NOTEBOOK(win->notebook), gtkconv->tab_cont, dest_page_num);
7503 } else {
7504 pidgin_conv_window_remove_gtkconv(win, gtkconv);
7505 pidgin_conv_window_add_gtkconv(dest_win, gtkconv);
7506 gtk_notebook_reorder_child(GTK_NOTEBOOK(dest_win->notebook), gtkconv->tab_cont, dest_page_num + to_right);
7507 pidgin_conv_window_switch_gtkconv(dest_win, gtkconv);
7508 if (new_window) {
7509 gint win_width, win_height;
7511 gtk_window_get_size(GTK_WINDOW(dest_win->window),
7512 &win_width, &win_height);
7513 #ifdef _WIN32 /* only override window manager placement on Windows */
7514 gtk_window_move(GTK_WINDOW(dest_win->window),
7515 e->x_root - (win_width / 2),
7516 e->y_root - (win_height / 2));
7517 #endif
7519 pidgin_conv_window_show(dest_win);
7523 gtk_widget_grab_focus(active_gtkconv->editor);
7525 return TRUE;
7529 static void
7530 before_switch_conv_cb(GtkNotebook *notebook, GtkWidget *page, gint page_num,
7531 gpointer user_data)
7533 PidginConvWindow *win;
7534 PurpleConversation *conv;
7535 PidginConversation *gtkconv;
7537 win = user_data;
7538 conv = pidgin_conv_window_get_active_conversation(win);
7540 g_return_if_fail(conv != NULL);
7542 if (!PURPLE_IS_IM_CONVERSATION(conv))
7543 return;
7545 gtkconv = PIDGIN_CONVERSATION(conv);
7547 if (gtkconv->u.im->typing_timer != 0) {
7548 g_source_remove(gtkconv->u.im->typing_timer);
7549 gtkconv->u.im->typing_timer = 0;
7552 stop_anim(NULL, gtkconv);
7555 static void
7556 close_window(GtkWidget *w, PidginConvWindow *win)
7558 close_win_cb(w, NULL, win);
7561 static void
7562 detach_tab_cb(GtkWidget *w, PidginConvWindow *win)
7564 PidginConvWindow *new_window;
7565 PidginConversation *gtkconv;
7567 gtkconv = win->clicked_tab;
7569 if (!gtkconv)
7570 return;
7572 /* Nothing to do if there's only one tab in the window */
7573 if (pidgin_conv_window_get_gtkconv_count(win) == 1)
7574 return;
7576 pidgin_conv_window_remove_gtkconv(win, gtkconv);
7578 new_window = pidgin_conv_window_new();
7579 pidgin_conv_window_add_gtkconv(new_window, gtkconv);
7580 pidgin_conv_window_show(new_window);
7583 static void
7584 close_others_cb(GtkWidget *w, PidginConvWindow *win)
7586 GList *iter;
7587 PidginConversation *gtkconv;
7589 gtkconv = win->clicked_tab;
7591 if (!gtkconv)
7592 return;
7594 for (iter = pidgin_conv_window_get_gtkconvs(win); iter; )
7596 PidginConversation *gconv = iter->data;
7597 iter = iter->next;
7599 if (gconv != gtkconv)
7601 close_conv_cb(NULL, gconv);
7606 static void
7607 close_tab_cb(GtkWidget *w, PidginConvWindow *win)
7609 PidginConversation *gtkconv;
7611 gtkconv = win->clicked_tab;
7613 if (gtkconv)
7614 close_conv_cb(NULL, gtkconv);
7617 static void
7618 notebook_menu_switch_cb(GtkWidget *item, GtkWidget *child)
7620 GtkNotebook *notebook;
7621 int index;
7623 notebook = GTK_NOTEBOOK(gtk_widget_get_parent(child));
7624 index = gtk_notebook_page_num(notebook, child);
7625 gtk_notebook_set_current_page(notebook, index);
7628 static void
7629 notebook_menu_update_label_cb(GtkWidget *child, GParamSpec *pspec,
7630 GtkNotebook *notebook)
7632 GtkWidget *item;
7633 GtkWidget *label;
7635 item = g_object_get_data(G_OBJECT(child), "popup-menu-item");
7636 label = gtk_bin_get_child(GTK_BIN(item));
7637 if (label)
7638 gtk_container_remove(GTK_CONTAINER(item), label);
7640 label = gtk_notebook_get_menu_label(notebook, child);
7641 if (label) {
7642 gtk_widget_show(label);
7643 gtk_container_add(GTK_CONTAINER(item), label);
7644 gtk_widget_show(item);
7645 } else {
7646 gtk_widget_hide(item);
7650 static void
7651 notebook_add_tab_to_menu_cb(GtkNotebook *notebook, GtkWidget *child,
7652 guint page_num, PidginConvWindow *win)
7654 GtkWidget *item;
7655 GtkWidget *label;
7657 item = gtk_menu_item_new();
7658 label = gtk_notebook_get_menu_label(notebook, child);
7659 if (label) {
7660 gtk_widget_show(label);
7661 gtk_container_add(GTK_CONTAINER(item), label);
7662 gtk_widget_show(item);
7665 g_signal_connect(child, "child-notify::menu-label",
7666 G_CALLBACK(notebook_menu_update_label_cb), notebook);
7667 g_signal_connect(item, "activate",
7668 G_CALLBACK(notebook_menu_switch_cb), child);
7669 g_object_set_data(G_OBJECT(child), "popup-menu-item", item);
7671 gtk_menu_shell_insert(GTK_MENU_SHELL(win->notebook_menu), item, page_num);
7674 static void
7675 notebook_remove_tab_from_menu_cb(GtkNotebook *notebook, GtkWidget *child,
7676 guint page_num, PidginConvWindow *win)
7678 GtkWidget *item;
7680 /* Disconnecting the "child-notify::menu-label" signal. */
7681 g_signal_handlers_disconnect_by_data(child, notebook);
7683 item = g_object_get_data(G_OBJECT(child), "popup-menu-item");
7684 gtk_container_remove(GTK_CONTAINER(win->notebook_menu), item);
7688 static void
7689 notebook_reorder_tab_in_menu_cb(GtkNotebook *notebook, GtkWidget *child,
7690 guint page_num, PidginConvWindow *win)
7692 GtkWidget *item;
7694 item = g_object_get_data(G_OBJECT(child), "popup-menu-item");
7695 gtk_menu_reorder_child(GTK_MENU(win->notebook_menu), item, page_num);
7698 static gboolean
7699 notebook_right_click_menu_cb(GtkNotebook *notebook, GdkEventButton *event,
7700 PidginConvWindow *win)
7702 GtkWidget *menu;
7703 PidginConversation *gtkconv;
7705 if (!gdk_event_triggers_context_menu((GdkEvent *)event))
7706 return FALSE;
7708 gtkconv = pidgin_conv_window_get_gtkconv_at_index(win,
7709 pidgin_conv_get_tab_at_xy(win, event->x_root, event->y_root, NULL));
7711 win->clicked_tab = gtkconv;
7713 menu = win->notebook_menu;
7715 gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent *)event);
7717 return TRUE;
7720 static void
7721 remove_edit_entry(PidginConversation *gtkconv, GtkWidget *entry)
7723 g_signal_handlers_disconnect_matched(G_OBJECT(entry), G_SIGNAL_MATCH_DATA,
7724 0, 0, NULL, NULL, gtkconv);
7725 gtk_widget_show(gtkconv->infopane);
7726 gtk_widget_grab_focus(gtkconv->editor);
7727 gtk_widget_destroy(entry);
7730 static gboolean
7731 alias_focus_cb(GtkWidget *widget, GdkEventFocus *event, gpointer user_data)
7733 remove_edit_entry(user_data, widget);
7734 return FALSE;
7737 static gboolean
7738 alias_key_press_cb(GtkWidget *widget, GdkEventKey *event, gpointer user_data)
7740 if (event->keyval == GDK_KEY_Escape) {
7741 remove_edit_entry(user_data, widget);
7742 return TRUE;
7744 return FALSE;
7747 static void
7748 alias_cb(GtkEntry *entry, gpointer user_data)
7750 PidginConversation *gtkconv;
7751 PurpleConversation *conv;
7752 PurpleAccount *account;
7753 const char *name;
7755 gtkconv = (PidginConversation *)user_data;
7756 if (gtkconv == NULL) {
7757 return;
7759 conv = gtkconv->active_conv;
7760 account = purple_conversation_get_account(conv);
7761 name = purple_conversation_get_name(conv);
7763 if (PURPLE_IS_IM_CONVERSATION(conv)) {
7764 PurpleBuddy *buddy;
7765 buddy = purple_blist_find_buddy(account, name);
7766 if (buddy != NULL) {
7767 purple_buddy_set_local_alias(buddy, gtk_entry_get_text(entry));
7769 purple_serv_alias_buddy(buddy);
7770 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
7771 gtk_entry_set_text(GTK_ENTRY(gtkconv->u.chat->topic_text), gtk_entry_get_text(entry));
7772 topic_callback(NULL, gtkconv);
7774 remove_edit_entry(user_data, GTK_WIDGET(entry));
7777 static gboolean
7778 infopane_entry_activate(PidginConversation *gtkconv)
7780 GtkWidget *entry = NULL;
7781 PurpleConversation *conv = gtkconv->active_conv;
7782 const char *text = NULL;
7784 if (!gtk_widget_get_visible(gtkconv->infopane)) {
7785 /* There's already an entry for alias. Let's not create another one. */
7786 return FALSE;
7789 if (!purple_account_is_connected(purple_conversation_get_account(gtkconv->active_conv))) {
7790 /* Do not allow aliasing someone on a disconnected account. */
7791 return FALSE;
7794 if (PURPLE_IS_IM_CONVERSATION(conv)) {
7795 PurpleBuddy *buddy = purple_blist_find_buddy(purple_conversation_get_account(gtkconv->active_conv), purple_conversation_get_name(gtkconv->active_conv));
7796 if (!buddy)
7797 /* This buddy isn't in your buddy list, so we can't alias him */
7798 return FALSE;
7800 text = purple_buddy_get_contact_alias(buddy);
7801 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
7802 PurpleConnection *gc;
7803 PurpleProtocol *protocol = NULL;
7805 gc = purple_conversation_get_connection(conv);
7806 if (gc != NULL)
7807 protocol = purple_connection_get_protocol(gc);
7808 if (protocol && !PURPLE_PROTOCOL_IMPLEMENTS(protocol, CHAT, set_topic))
7809 /* This protocol doesn't support setting the chat room topic */
7810 return FALSE;
7812 text = purple_chat_conversation_get_topic(PURPLE_CHAT_CONVERSATION(conv));
7815 /* alias label */
7816 entry = gtk_entry_new();
7817 gtk_entry_set_has_frame(GTK_ENTRY(entry), FALSE);
7818 gtk_entry_set_width_chars(GTK_ENTRY(entry), 10);
7819 gtk_entry_set_alignment(GTK_ENTRY(entry), 0.5);
7821 gtk_box_pack_start(GTK_BOX(gtkconv->infopane_hbox), entry, TRUE, TRUE, 0);
7822 /* after the tab label */
7823 gtk_box_reorder_child(GTK_BOX(gtkconv->infopane_hbox), entry, 0);
7825 g_signal_connect(G_OBJECT(entry), "activate", G_CALLBACK(alias_cb), gtkconv);
7826 g_signal_connect(G_OBJECT(entry), "focus-out-event", G_CALLBACK(alias_focus_cb), gtkconv);
7827 g_signal_connect(G_OBJECT(entry), "key-press-event", G_CALLBACK(alias_key_press_cb), gtkconv);
7829 if (text != NULL)
7830 gtk_entry_set_text(GTK_ENTRY(entry), text);
7831 gtk_widget_show(entry);
7832 gtk_widget_hide(gtkconv->infopane);
7833 gtk_widget_grab_focus(entry);
7835 return TRUE;
7838 static gboolean
7839 window_keypress_cb(GtkWidget *widget, GdkEventKey *event, PidginConvWindow *win)
7841 PidginConversation *gtkconv = pidgin_conv_window_get_active_gtkconv(win);
7843 return conv_keypress_common(gtkconv, event);
7846 static void
7847 switch_conv_cb(GtkNotebook *notebook, GtkWidget *page, gint page_num,
7848 gpointer user_data)
7850 PidginConvWindow *win;
7851 PurpleConversation *conv;
7852 PidginConversation *gtkconv;
7853 const char *sound_method;
7855 win = user_data;
7856 gtkconv = pidgin_conv_window_get_gtkconv_at_index(win, page_num);
7857 conv = gtkconv->active_conv;
7859 g_return_if_fail(conv != NULL);
7861 /* clear unseen flag if conversation is not hidden */
7862 if(!pidgin_conv_is_hidden(gtkconv)) {
7863 gtkconv_set_unseen(gtkconv, PIDGIN_UNSEEN_NONE);
7866 /* Update the menubar */
7868 gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(gtkconv->win->menu->logging),
7869 purple_conversation_is_logging(conv));
7871 generate_send_to_items(win);
7872 generate_e2ee_controls(win);
7873 regenerate_options_items(win);
7874 regenerate_plugins_items(win);
7876 pidgin_conv_switch_active_conversation(conv);
7878 sound_method = purple_prefs_get_string(PIDGIN_PREFS_ROOT "/sound/method");
7879 if (!purple_strequal(sound_method, "none"))
7880 gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(win->menu->sounds),
7881 gtkconv->make_sound);
7883 gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(win->menu->show_formatting_toolbar),
7884 purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/show_formatting_toolbar"));
7887 * We pause icons when they are not visible. If this icon should
7888 * be animated then start it back up again.
7890 if (PURPLE_IS_IM_CONVERSATION(conv) &&
7891 (gtkconv->u.im->animate))
7892 start_anim(NULL, gtkconv);
7894 purple_signal_emit(pidgin_conversations_get_handle(), "conversation-switched", conv);
7897 /**************************************************************************
7898 * GTK+ window ops
7899 **************************************************************************/
7901 GList *
7902 pidgin_conv_windows_get_list()
7904 return window_list;
7907 static GList*
7908 make_status_icon_list(const char *stock, GtkWidget *w)
7910 GList *l = NULL;
7911 l = g_list_append(l,
7912 gtk_widget_render_icon(w, stock,
7913 gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_EXTRA_SMALL), "GtkWindow"));
7914 l = g_list_append(l,
7915 gtk_widget_render_icon(w, stock,
7916 gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_SMALL), "GtkWindow"));
7917 l = g_list_append(l,
7918 gtk_widget_render_icon(w, stock,
7919 gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_MEDIUM), "GtkWindow"));
7920 l = g_list_append(l,
7921 gtk_widget_render_icon(w, stock,
7922 gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_LARGE), "GtkWindow"));
7923 return l;
7926 static void
7927 create_icon_lists(GtkWidget *w)
7929 available_list = make_status_icon_list(PIDGIN_STOCK_STATUS_AVAILABLE, w);
7930 busy_list = make_status_icon_list(PIDGIN_STOCK_STATUS_BUSY, w);
7931 xa_list = make_status_icon_list(PIDGIN_STOCK_STATUS_XA, w);
7932 offline_list = make_status_icon_list(PIDGIN_STOCK_STATUS_OFFLINE, w);
7933 away_list = make_status_icon_list(PIDGIN_STOCK_STATUS_AWAY, w);
7934 protocol_lists = g_hash_table_new(g_str_hash, g_str_equal);
7937 static void
7938 plugin_changed_cb(PurplePlugin *p, gpointer data)
7940 regenerate_plugins_items(data);
7943 static gboolean gtk_conv_configure_cb(GtkWidget *w, GdkEventConfigure *event, gpointer data) {
7944 int x, y;
7946 if (gtk_widget_get_visible(w))
7947 gtk_window_get_position(GTK_WINDOW(w), &x, &y);
7948 else
7949 return FALSE; /* carry on normally */
7951 /* Workaround for GTK+ bug # 169811 - "configure_event" is fired
7952 * when the window is being maximized */
7953 if (gdk_window_get_state(gtk_widget_get_window(w)) & GDK_WINDOW_STATE_MAXIMIZED)
7954 return FALSE;
7956 /* don't save off-screen positioning */
7957 if (x + event->width < 0 ||
7958 y + event->height < 0 ||
7959 x > gdk_screen_width() ||
7960 y > gdk_screen_height())
7961 return FALSE; /* carry on normally */
7963 /* store the position */
7964 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/x", x);
7965 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/y", y);
7966 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/width", event->width);
7967 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/height", event->height);
7969 /* continue to handle event normally */
7970 return FALSE;
7974 static void
7975 pidgin_conv_set_position_size(PidginConvWindow *win, int conv_x, int conv_y,
7976 int conv_width, int conv_height)
7978 /* if the window exists, is hidden, we're saving positions, and the
7979 * position is sane... */
7980 if (win && win->window &&
7981 !gtk_widget_get_visible(win->window) && conv_width != 0) {
7983 #ifdef _WIN32 /* only override window manager placement on Windows */
7984 /* ...check position is on screen... */
7985 if (conv_x >= gdk_screen_width())
7986 conv_x = gdk_screen_width() - 100;
7987 else if (conv_x + conv_width < 0)
7988 conv_x = 100;
7990 if (conv_y >= gdk_screen_height())
7991 conv_y = gdk_screen_height() - 100;
7992 else if (conv_y + conv_height < 0)
7993 conv_y = 100;
7995 /* ...and move it back. */
7996 gtk_window_move(GTK_WINDOW(win->window), conv_x, conv_y);
7997 #endif
7998 gtk_window_resize(GTK_WINDOW(win->window), conv_width, conv_height);
8002 static void
8003 pidgin_conv_restore_position(PidginConvWindow *win) {
8004 pidgin_conv_set_position_size(win,
8005 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/x"),
8006 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/y"),
8007 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/width"),
8008 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/height"));
8011 PidginConvWindow *
8012 pidgin_conv_window_new()
8014 PidginConvWindow *win;
8015 GtkPositionType pos;
8016 GtkWidget *testidea;
8017 GtkWidget *menubar;
8018 GtkWidget *menu;
8019 GtkWidget *item;
8020 GdkModifierType state;
8022 win = g_malloc0(sizeof(PidginConvWindow));
8023 win->menu = g_malloc0(sizeof(PidginConvWindowMenu));
8025 window_list = g_list_append(window_list, win);
8027 /* Create the window. */
8028 win->window = pidgin_create_window(NULL, 0, "conversation", TRUE);
8029 /*_pidgin_widget_set_accessible_name(win->window, "Conversations");*/
8030 if (!gtk_get_current_event_state(&state))
8031 gtk_window_set_focus_on_map(GTK_WINDOW(win->window), FALSE);
8033 /* Etan: I really think this entire function call should happen only
8034 * when we are on Windows but I was informed that back before we used
8035 * to save the window position we stored the window size, so I'm
8036 * leaving it for now. */
8037 #if TRUE || defined(_WIN32)
8038 pidgin_conv_restore_position(win);
8039 #endif
8041 if (available_list == NULL) {
8042 create_icon_lists(win->window);
8045 g_signal_connect(G_OBJECT(win->window), "delete_event",
8046 G_CALLBACK(close_win_cb), win);
8047 g_signal_connect(G_OBJECT(win->window), "focus_in_event",
8048 G_CALLBACK(focus_win_cb), win);
8050 /* Intercept keystrokes from the menu items */
8051 g_signal_connect(G_OBJECT(win->window), "key_press_event",
8052 G_CALLBACK(window_keypress_cb), win);
8055 /* Create the notebook. */
8056 win->notebook = gtk_notebook_new();
8058 pos = purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side");
8060 gtk_notebook_set_tab_pos(GTK_NOTEBOOK(win->notebook), pos);
8061 gtk_notebook_set_scrollable(GTK_NOTEBOOK(win->notebook), TRUE);
8062 gtk_notebook_set_show_tabs(GTK_NOTEBOOK(win->notebook), FALSE);
8063 gtk_notebook_set_show_border(GTK_NOTEBOOK(win->notebook), TRUE);
8065 menu = win->notebook_menu = gtk_menu_new();
8067 pidgin_separator(GTK_WIDGET(menu));
8069 item = gtk_menu_item_new_with_label(_("Close other tabs"));
8070 gtk_widget_show(item);
8071 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
8072 g_signal_connect(G_OBJECT(item), "activate",
8073 G_CALLBACK(close_others_cb), win);
8075 item = gtk_menu_item_new_with_label(_("Close all tabs"));
8076 gtk_widget_show(item);
8077 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
8078 g_signal_connect(G_OBJECT(item), "activate",
8079 G_CALLBACK(close_window), win);
8081 pidgin_separator(menu);
8083 item = gtk_menu_item_new_with_label(_("Detach this tab"));
8084 gtk_widget_show(item);
8085 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
8086 g_signal_connect(G_OBJECT(item), "activate",
8087 G_CALLBACK(detach_tab_cb), win);
8089 item = gtk_menu_item_new_with_label(_("Close this tab"));
8090 gtk_widget_show(item);
8091 gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
8092 g_signal_connect(G_OBJECT(item), "activate",
8093 G_CALLBACK(close_tab_cb), win);
8095 g_signal_connect(G_OBJECT(win->notebook), "page-added",
8096 G_CALLBACK(notebook_add_tab_to_menu_cb), win);
8097 g_signal_connect(G_OBJECT(win->notebook), "page-removed",
8098 G_CALLBACK(notebook_remove_tab_from_menu_cb), win);
8099 g_signal_connect(G_OBJECT(win->notebook), "page-reordered",
8100 G_CALLBACK(notebook_reorder_tab_in_menu_cb), win);
8102 g_signal_connect(G_OBJECT(win->notebook), "button-press-event",
8103 G_CALLBACK(notebook_right_click_menu_cb), win);
8105 gtk_widget_show(win->notebook);
8107 g_signal_connect(G_OBJECT(win->notebook), "switch_page",
8108 G_CALLBACK(before_switch_conv_cb), win);
8109 g_signal_connect_after(G_OBJECT(win->notebook), "switch_page",
8110 G_CALLBACK(switch_conv_cb), win);
8112 /* Setup the tab drag and drop signals. */
8113 gtk_widget_add_events(win->notebook,
8114 GDK_BUTTON1_MOTION_MASK | GDK_LEAVE_NOTIFY_MASK);
8115 g_signal_connect(G_OBJECT(win->notebook), "button_press_event",
8116 G_CALLBACK(notebook_press_cb), win);
8117 g_signal_connect(G_OBJECT(win->notebook), "button_release_event",
8118 G_CALLBACK(notebook_release_cb), win);
8120 testidea = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
8122 /* Setup the menubar. */
8123 menubar = setup_menubar(win);
8124 gtk_box_pack_start(GTK_BOX(testidea), menubar, FALSE, TRUE, 0);
8126 gtk_box_pack_start(GTK_BOX(testidea), win->notebook, TRUE, TRUE, 0);
8128 gtk_container_add(GTK_CONTAINER(win->window), testidea);
8130 gtk_widget_show(testidea);
8132 /* Update the plugin actions when plugins are (un)loaded */
8133 purple_signal_connect(purple_plugins_get_handle(), "plugin-load",
8134 win, PURPLE_CALLBACK(plugin_changed_cb), win);
8135 purple_signal_connect(purple_plugins_get_handle(), "plugin-unload",
8136 win, PURPLE_CALLBACK(plugin_changed_cb), win);
8139 #ifdef _WIN32
8140 g_signal_connect(G_OBJECT(win->window), "show",
8141 G_CALLBACK(winpidgin_ensure_onscreen), win->window);
8143 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/win32/minimize_new_convs")
8144 && !gtk_get_current_event_state(&state))
8145 gtk_window_iconify(GTK_WINDOW(win->window));
8146 #endif
8148 purple_signal_emit(pidgin_conversations_get_handle(),
8149 "conversation-window-created", win);
8151 /* Fix colours */
8152 pidgin_conversations_set_tab_colors();
8154 return win;
8157 void
8158 pidgin_conv_window_destroy(PidginConvWindow *win)
8160 if (win->gtkconvs) {
8161 GList *iter = win->gtkconvs;
8162 while (iter)
8164 PidginConversation *gtkconv = iter->data;
8165 iter = iter->next;
8166 close_conv_cb(NULL, gtkconv);
8168 return;
8171 purple_prefs_disconnect_by_handle(win);
8172 window_list = g_list_remove(window_list, win);
8174 gtk_widget_destroy(win->notebook_menu);
8175 gtk_widget_destroy(win->window);
8177 g_object_unref(G_OBJECT(win->menu->ui));
8179 purple_notify_close_with_handle(win);
8180 purple_signals_disconnect_by_handle(win);
8182 g_free(win->menu);
8183 g_free(win);
8186 void
8187 pidgin_conv_window_show(PidginConvWindow *win)
8189 gtk_widget_show(win->window);
8192 void
8193 pidgin_conv_window_hide(PidginConvWindow *win)
8195 gtk_widget_hide(win->window);
8198 void
8199 pidgin_conv_window_raise(PidginConvWindow *win)
8201 gdk_window_raise(GDK_WINDOW(gtk_widget_get_window(win->window)));
8204 void
8205 pidgin_conv_window_switch_gtkconv(PidginConvWindow *win, PidginConversation *gtkconv)
8207 gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook),
8208 gtk_notebook_page_num(GTK_NOTEBOOK(win->notebook),
8209 gtkconv->tab_cont));
8212 static gboolean
8213 gtkconv_tab_set_tip(GtkWidget *widget, GdkEventCrossing *event, PidginConversation *gtkconv)
8215 /* PANGO_VERSION_CHECK macro was introduced in 1.15. So we need this double check. */
8216 #ifndef PANGO_VERSION_CHECK
8217 #define pango_layout_is_ellipsized(l) TRUE
8218 #elif !PANGO_VERSION_CHECK(1,16,0)
8219 #define pango_layout_is_ellipsized(l) TRUE
8220 #endif
8221 PangoLayout *layout;
8223 layout = gtk_label_get_layout(GTK_LABEL(gtkconv->tab_label));
8224 if (pango_layout_is_ellipsized(layout))
8225 gtk_widget_set_tooltip_text(widget, gtk_label_get_text(GTK_LABEL(gtkconv->tab_label)));
8226 else
8227 gtk_widget_set_tooltip_text(widget, NULL);
8229 return FALSE;
8232 static void
8233 set_default_tab_colors(GtkWidget *widget)
8235 GString *str;
8236 GtkCssProvider *provider;
8237 GError *error = NULL;
8238 int iter;
8240 struct {
8241 const char *labelname;
8242 const char *color;
8243 } styles[] = {
8244 {"tab-label-typing", "#4e9a06"},
8245 {"tab-label-typed", "#c4a000"},
8246 {"tab-label-attention", "#006aff"},
8247 {"tab-label-unreadchat", "#cc0000"},
8248 {"tab-label-event", "#888a85"},
8249 {NULL, NULL}
8252 str = g_string_new(NULL);
8254 for (iter = 0; styles[iter].labelname; iter++) {
8255 g_string_append_printf(str,
8256 "#%s {\n"
8257 " color: %s;\n"
8258 "}\n",
8259 styles[iter].labelname,
8260 styles[iter].color);
8263 provider = gtk_css_provider_new();
8265 gtk_css_provider_load_from_data(provider, str->str, str->len, &error);
8267 gtk_style_context_add_provider(gtk_widget_get_style_context(widget),
8268 GTK_STYLE_PROVIDER(provider),
8269 GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
8271 if (error)
8272 g_error_free(error);
8273 g_string_free(str, TRUE);
8276 void
8277 pidgin_conv_window_add_gtkconv(PidginConvWindow *win, PidginConversation *gtkconv)
8279 PurpleConversation *conv = gtkconv->active_conv;
8280 PidginConversation *focus_gtkconv;
8281 GtkWidget *tab_cont = gtkconv->tab_cont;
8282 const gchar *tmp_lab;
8284 win->gtkconvs = g_list_append(win->gtkconvs, gtkconv);
8285 gtkconv->win = win;
8287 if (win->gtkconvs && win->gtkconvs->next && win->gtkconvs->next->next == NULL)
8288 pidgin_conv_tab_pack(win, ((PidginConversation*)win->gtkconvs->data));
8291 /* Close button. */
8292 gtkconv->close = pidgin_create_small_button(gtk_label_new("×"));
8293 gtk_widget_set_tooltip_text(gtkconv->close, _("Close conversation"));
8295 g_signal_connect(gtkconv->close, "clicked", G_CALLBACK (close_conv_cb), gtkconv);
8297 /* Status icon. */
8298 gtkconv->icon = gtk_image_new();
8299 gtkconv->menu_icon = gtk_image_new();
8300 g_object_set(G_OBJECT(gtkconv->icon),
8301 "icon-size", gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_MICROSCOPIC),
8302 NULL);
8303 g_object_set(G_OBJECT(gtkconv->menu_icon),
8304 "icon-size", gtk_icon_size_from_name(PIDGIN_ICON_SIZE_TANGO_MICROSCOPIC),
8305 NULL);
8306 gtk_widget_show(gtkconv->icon);
8307 update_tab_icon(conv);
8309 /* Tab label. */
8310 gtkconv->tab_label = gtk_label_new(tmp_lab = purple_conversation_get_title(conv));
8311 set_default_tab_colors(gtkconv->tab_label);
8312 gtk_widget_set_name(gtkconv->tab_label, "tab-label");
8314 gtkconv->menu_tabby = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, PIDGIN_HIG_BOX_SPACE);
8315 gtkconv->menu_label = gtk_label_new(tmp_lab);
8316 gtk_box_pack_start(GTK_BOX(gtkconv->menu_tabby), gtkconv->menu_icon, FALSE, FALSE, 0);
8318 gtk_widget_show_all(gtkconv->menu_icon);
8320 gtk_box_pack_start(GTK_BOX(gtkconv->menu_tabby), gtkconv->menu_label, TRUE, TRUE, 0);
8321 gtk_widget_show(gtkconv->menu_label);
8322 gtk_label_set_xalign(GTK_LABEL(gtkconv->menu_label), 0);
8323 gtk_label_set_yalign(GTK_LABEL(gtkconv->menu_label), 0);
8325 gtk_widget_show(gtkconv->menu_tabby);
8327 if (PURPLE_IS_IM_CONVERSATION(conv))
8328 pidgin_conv_update_buddy_icon(PURPLE_IM_CONVERSATION(conv));
8330 /* Build and set conversations tab */
8331 pidgin_conv_tab_pack(win, gtkconv);
8333 gtk_notebook_set_menu_label(GTK_NOTEBOOK(win->notebook), tab_cont, gtkconv->menu_tabby);
8335 gtk_widget_show(tab_cont);
8337 if (pidgin_conv_window_get_gtkconv_count(win) == 1) {
8338 /* Er, bug in notebooks? Switch to the page manually. */
8339 gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook), 0);
8340 } else {
8341 gtk_notebook_set_show_tabs(GTK_NOTEBOOK(win->notebook), TRUE);
8344 focus_gtkconv = g_list_nth_data(pidgin_conv_window_get_gtkconvs(win),
8345 gtk_notebook_get_current_page(GTK_NOTEBOOK(win->notebook)));
8346 gtk_widget_grab_focus(focus_gtkconv->editor);
8348 if (pidgin_conv_window_get_gtkconv_count(win) == 1)
8349 update_send_to_selection(win);
8352 static void
8353 pidgin_conv_tab_pack(PidginConvWindow *win, PidginConversation *gtkconv)
8355 gboolean tabs_side = FALSE;
8356 gint angle = 0;
8357 GtkWidget *first, *third, *ebox, *parent;
8359 if (purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side") == GTK_POS_LEFT ||
8360 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side") == GTK_POS_RIGHT)
8361 tabs_side = TRUE;
8362 else if (purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side") == (GTK_POS_LEFT|8))
8363 angle = 90;
8364 else if (purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side") == (GTK_POS_RIGHT|8))
8365 angle = 270;
8367 if (!angle) {
8368 g_object_set(G_OBJECT(gtkconv->tab_label), "ellipsize", PANGO_ELLIPSIZE_END, NULL);
8369 gtk_label_set_width_chars(GTK_LABEL(gtkconv->tab_label), 4);
8370 } else {
8371 g_object_set(G_OBJECT(gtkconv->tab_label), "ellipsize", PANGO_ELLIPSIZE_NONE, NULL);
8372 gtk_label_set_width_chars(GTK_LABEL(gtkconv->tab_label), -1);
8375 if (tabs_side) {
8376 gtk_label_set_width_chars(
8377 GTK_LABEL(gtkconv->tab_label),
8378 MIN(g_utf8_strlen(gtk_label_get_text(GTK_LABEL(gtkconv->tab_label)), -1), 12)
8382 gtk_label_set_angle(GTK_LABEL(gtkconv->tab_label), angle);
8384 if (angle)
8385 gtkconv->tabby = gtk_box_new(GTK_ORIENTATION_VERTICAL, PIDGIN_HIG_BOX_SPACE);
8386 else
8387 gtkconv->tabby = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, PIDGIN_HIG_BOX_SPACE);
8388 gtk_widget_set_name(gtkconv->tabby, "tab-container");
8390 /* select the correct ordering for verticle tabs */
8391 if (angle == 90) {
8392 first = gtkconv->close;
8393 third = gtkconv->icon;
8394 } else {
8395 first = gtkconv->icon;
8396 third = gtkconv->close;
8399 ebox = gtk_event_box_new();
8400 gtk_event_box_set_visible_window(GTK_EVENT_BOX(ebox), FALSE);
8401 gtk_container_add(GTK_CONTAINER(ebox), gtkconv->tabby);
8402 g_signal_connect(G_OBJECT(ebox), "enter-notify-event",
8403 G_CALLBACK(gtkconv_tab_set_tip), gtkconv);
8405 parent = gtk_widget_get_parent(gtkconv->tab_label);
8406 if (parent != NULL) {
8407 /* reparent old widgets on preference changes */
8408 g_object_ref(first);
8409 g_object_ref(gtkconv->tab_label);
8410 g_object_ref(third);
8411 gtk_container_remove(GTK_CONTAINER(parent), first);
8412 gtk_container_remove(GTK_CONTAINER(parent), gtkconv->tab_label);
8413 gtk_container_remove(GTK_CONTAINER(parent), third);
8416 gtk_box_pack_start(GTK_BOX(gtkconv->tabby), first, FALSE, FALSE, 0);
8417 gtk_box_pack_start(GTK_BOX(gtkconv->tabby), gtkconv->tab_label, TRUE, TRUE, 0);
8418 gtk_box_pack_start(GTK_BOX(gtkconv->tabby), third, FALSE, FALSE, 0);
8420 if (parent == NULL) {
8421 /* Add this pane to the conversation's notebook. */
8422 gtk_notebook_append_page(GTK_NOTEBOOK(win->notebook), gtkconv->tab_cont, ebox);
8423 } else {
8424 /* reparent old widgets on preference changes */
8425 g_object_unref(first);
8426 g_object_unref(gtkconv->tab_label);
8427 g_object_unref(third);
8429 /* Reset the tabs label to the new version */
8430 gtk_notebook_set_tab_label(GTK_NOTEBOOK(win->notebook), gtkconv->tab_cont, ebox);
8433 gtk_container_child_set(GTK_CONTAINER(win->notebook), gtkconv->tab_cont,
8434 "tab-expand", !tabs_side && !angle,
8435 "tab-fill", TRUE, NULL);
8437 if (pidgin_conv_window_get_gtkconv_count(win) == 1)
8438 gtk_notebook_set_show_tabs(GTK_NOTEBOOK(win->notebook),
8439 purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/tabs") &&
8440 (!purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/im/show_buddy_icons") ||
8441 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side") != GTK_POS_TOP));
8443 /* show the widgets */
8444 /* gtk_widget_show(gtkconv->icon); */
8445 gtk_widget_show(gtkconv->tab_label);
8446 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/close_on_tabs"))
8447 gtk_widget_show(gtkconv->close);
8448 gtk_widget_show(gtkconv->tabby);
8449 gtk_widget_show(ebox);
8452 void
8453 pidgin_conv_window_remove_gtkconv(PidginConvWindow *win, PidginConversation *gtkconv)
8455 unsigned int index;
8457 index = gtk_notebook_page_num(GTK_NOTEBOOK(win->notebook), gtkconv->tab_cont);
8459 g_object_ref_sink(G_OBJECT(gtkconv->tab_cont));
8461 gtk_notebook_remove_page(GTK_NOTEBOOK(win->notebook), index);
8463 win->gtkconvs = g_list_remove(win->gtkconvs, gtkconv);
8465 g_signal_handlers_disconnect_matched(win->window, G_SIGNAL_MATCH_DATA,
8466 0, 0, NULL, NULL, gtkconv);
8468 if (win->gtkconvs && win->gtkconvs->next == NULL)
8469 pidgin_conv_tab_pack(win, win->gtkconvs->data);
8471 if (!win->gtkconvs && win != hidden_convwin)
8472 pidgin_conv_window_destroy(win);
8475 PidginConversation *
8476 pidgin_conv_window_get_gtkconv_at_index(const PidginConvWindow *win, int index)
8478 GtkWidget *tab_cont;
8480 if (index == -1)
8481 index = 0;
8482 tab_cont = gtk_notebook_get_nth_page(GTK_NOTEBOOK(win->notebook), index);
8483 return tab_cont ? g_object_get_data(G_OBJECT(tab_cont), "PidginConversation") : NULL;
8486 PidginConversation *
8487 pidgin_conv_window_get_active_gtkconv(const PidginConvWindow *win)
8489 int index;
8490 GtkWidget *tab_cont;
8492 index = gtk_notebook_get_current_page(GTK_NOTEBOOK(win->notebook));
8493 if (index == -1)
8494 index = 0;
8495 tab_cont = gtk_notebook_get_nth_page(GTK_NOTEBOOK(win->notebook), index);
8496 if (!tab_cont)
8497 return NULL;
8498 return g_object_get_data(G_OBJECT(tab_cont), "PidginConversation");
8502 PurpleConversation *
8503 pidgin_conv_window_get_active_conversation(const PidginConvWindow *win)
8505 PidginConversation *gtkconv;
8507 gtkconv = pidgin_conv_window_get_active_gtkconv(win);
8508 return gtkconv ? gtkconv->active_conv : NULL;
8511 gboolean
8512 pidgin_conv_window_is_active_conversation(const PurpleConversation *conv)
8514 return conv == pidgin_conv_window_get_active_conversation(PIDGIN_CONVERSATION(conv)->win);
8517 gboolean
8518 pidgin_conv_window_has_focus(PidginConvWindow *win)
8520 gboolean has_focus = FALSE;
8522 g_object_get(G_OBJECT(win->window), "has-toplevel-focus", &has_focus, NULL);
8524 return has_focus;
8527 PidginConvWindow *
8528 pidgin_conv_window_get_at_event(GdkEvent *event)
8530 PidginConvWindow *win;
8531 GdkWindow *gdkwin;
8532 GList *l;
8533 int x, y;
8535 gdkwin = gdk_device_get_window_at_position(gdk_event_get_device(event),
8536 &x, &y);
8538 if (gdkwin)
8539 gdkwin = gdk_window_get_toplevel(gdkwin);
8541 for (l = pidgin_conv_windows_get_list(); l != NULL; l = l->next) {
8542 win = l->data;
8544 if (gdkwin == gtk_widget_get_window(win->window))
8545 return win;
8548 return NULL;
8551 GList *
8552 pidgin_conv_window_get_gtkconvs(PidginConvWindow *win)
8554 return win->gtkconvs;
8557 guint
8558 pidgin_conv_window_get_gtkconv_count(PidginConvWindow *win)
8560 return g_list_length(win->gtkconvs);
8563 PidginConvWindow *
8564 pidgin_conv_window_first_im(void)
8566 GList *wins, *convs;
8567 PidginConvWindow *win;
8568 PidginConversation *conv;
8570 for (wins = pidgin_conv_windows_get_list(); wins != NULL; wins = wins->next) {
8571 win = wins->data;
8573 for (convs = win->gtkconvs;
8574 convs != NULL;
8575 convs = convs->next) {
8577 conv = convs->data;
8579 if (PURPLE_IS_IM_CONVERSATION(conv->active_conv))
8580 return win;
8584 return NULL;
8587 PidginConvWindow *
8588 pidgin_conv_window_last_im(void)
8590 GList *wins, *convs;
8591 PidginConvWindow *win;
8592 PidginConversation *conv;
8594 for (wins = g_list_last(pidgin_conv_windows_get_list());
8595 wins != NULL;
8596 wins = wins->prev) {
8598 win = wins->data;
8600 for (convs = win->gtkconvs;
8601 convs != NULL;
8602 convs = convs->next) {
8604 conv = convs->data;
8606 if (PURPLE_IS_IM_CONVERSATION(conv->active_conv))
8607 return win;
8611 return NULL;
8614 PidginConvWindow *
8615 pidgin_conv_window_first_chat(void)
8617 GList *wins, *convs;
8618 PidginConvWindow *win;
8619 PidginConversation *conv;
8621 for (wins = pidgin_conv_windows_get_list(); wins != NULL; wins = wins->next) {
8622 win = wins->data;
8624 for (convs = win->gtkconvs;
8625 convs != NULL;
8626 convs = convs->next) {
8628 conv = convs->data;
8630 if (PURPLE_IS_CHAT_CONVERSATION(conv->active_conv))
8631 return win;
8635 return NULL;
8638 PidginConvWindow *
8639 pidgin_conv_window_last_chat(void)
8641 GList *wins, *convs;
8642 PidginConvWindow *win;
8643 PidginConversation *conv;
8645 for (wins = g_list_last(pidgin_conv_windows_get_list());
8646 wins != NULL;
8647 wins = wins->prev) {
8649 win = wins->data;
8651 for (convs = win->gtkconvs;
8652 convs != NULL;
8653 convs = convs->next) {
8655 conv = convs->data;
8657 if (PURPLE_IS_CHAT_CONVERSATION(conv->active_conv))
8658 return win;
8662 return NULL;
8666 /**************************************************************************
8667 * Conversation placement functions
8668 **************************************************************************/
8669 typedef struct
8671 char *id;
8672 char *name;
8673 PidginConvPlacementFunc fnc;
8675 } ConvPlacementData;
8677 static GList *conv_placement_fncs = NULL;
8678 static PidginConvPlacementFunc place_conv = NULL;
8680 /* This one places conversations in the last made window. */
8681 static void
8682 conv_placement_last_created_win(PidginConversation *conv)
8684 PidginConvWindow *win;
8686 GList *l = g_list_last(pidgin_conv_windows_get_list());
8687 win = l ? l->data : NULL;;
8689 if (win == NULL) {
8690 win = pidgin_conv_window_new();
8692 g_signal_connect(G_OBJECT(win->window), "configure_event",
8693 G_CALLBACK(gtk_conv_configure_cb), NULL);
8695 pidgin_conv_window_add_gtkconv(win, conv);
8696 pidgin_conv_window_show(win);
8697 } else {
8698 pidgin_conv_window_add_gtkconv(win, conv);
8702 /* This one places conversations in the last made window of the same type. */
8703 static gboolean
8704 conv_placement_last_created_win_type_configured_cb(GtkWidget *w,
8705 GdkEventConfigure *event, PidginConversation *conv)
8707 int x, y;
8708 GList *all;
8710 if (gtk_widget_get_visible(w))
8711 gtk_window_get_position(GTK_WINDOW(w), &x, &y);
8712 else
8713 return FALSE; /* carry on normally */
8715 /* Workaround for GTK+ bug # 169811 - "configure_event" is fired
8716 * when the window is being maximized */
8717 if (gdk_window_get_state(gtk_widget_get_window(w)) & GDK_WINDOW_STATE_MAXIMIZED)
8718 return FALSE;
8720 /* don't save off-screen positioning */
8721 if (x + event->width < 0 ||
8722 y + event->height < 0 ||
8723 x > gdk_screen_width() ||
8724 y > gdk_screen_height())
8725 return FALSE; /* carry on normally */
8727 for (all = conv->convs; all != NULL; all = all->next) {
8728 if (PURPLE_IS_IM_CONVERSATION(conv->active_conv) != PURPLE_IS_IM_CONVERSATION(all->data)) {
8729 /* this window has different types of conversation, don't save */
8730 return FALSE;
8734 if (PURPLE_IS_IM_CONVERSATION(conv->active_conv)) {
8735 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/x", x);
8736 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/y", y);
8737 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/width", event->width);
8738 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/im/height", event->height);
8739 } else if (PURPLE_IS_CHAT_CONVERSATION(conv->active_conv)) {
8740 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/chat/x", x);
8741 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/chat/y", y);
8742 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/chat/width", event->width);
8743 purple_prefs_set_int(PIDGIN_PREFS_ROOT "/conversations/chat/height", event->height);
8746 return FALSE;
8749 static void
8750 conv_placement_last_created_win_type(PidginConversation *conv)
8752 PidginConvWindow *win;
8754 if (PURPLE_IS_IM_CONVERSATION(conv->active_conv))
8755 win = pidgin_conv_window_last_im();
8756 else
8757 win = pidgin_conv_window_last_chat();
8759 if (win == NULL) {
8760 win = pidgin_conv_window_new();
8762 if (PURPLE_IS_IM_CONVERSATION(conv->active_conv) ||
8763 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/chat/width") == 0) {
8764 pidgin_conv_set_position_size(win,
8765 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/x"),
8766 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/y"),
8767 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/width"),
8768 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/height"));
8769 } else if (PURPLE_IS_CHAT_CONVERSATION(conv->active_conv)) {
8770 pidgin_conv_set_position_size(win,
8771 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/chat/x"),
8772 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/chat/y"),
8773 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/chat/width"),
8774 purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/chat/height"));
8777 pidgin_conv_window_add_gtkconv(win, conv);
8778 pidgin_conv_window_show(win);
8780 g_signal_connect(G_OBJECT(win->window), "configure_event",
8781 G_CALLBACK(conv_placement_last_created_win_type_configured_cb), conv);
8782 } else
8783 pidgin_conv_window_add_gtkconv(win, conv);
8786 /* This one places each conversation in its own window. */
8787 static void
8788 conv_placement_new_window(PidginConversation *conv)
8790 PidginConvWindow *win;
8792 win = pidgin_conv_window_new();
8794 g_signal_connect(G_OBJECT(win->window), "configure_event",
8795 G_CALLBACK(gtk_conv_configure_cb), NULL);
8797 pidgin_conv_window_add_gtkconv(win, conv);
8799 pidgin_conv_window_show(win);
8802 static PurpleGroup *
8803 conv_get_group(PidginConversation *conv)
8805 PurpleGroup *group = NULL;
8807 if (PURPLE_IS_IM_CONVERSATION(conv->active_conv)) {
8808 PurpleBuddy *buddy;
8810 buddy = purple_blist_find_buddy(purple_conversation_get_account(conv->active_conv),
8811 purple_conversation_get_name(conv->active_conv));
8813 if (buddy != NULL)
8814 group = purple_buddy_get_group(buddy);
8816 } else if (PURPLE_IS_CHAT_CONVERSATION(conv->active_conv)) {
8817 PurpleChat *chat;
8819 chat = purple_blist_find_chat(purple_conversation_get_account(conv->active_conv),
8820 purple_conversation_get_name(conv->active_conv));
8822 if (chat != NULL)
8823 group = purple_chat_get_group(chat);
8826 return group;
8830 * This groups things by, well, group. Buddies from groups will always be
8831 * grouped together, and a buddy from a group not belonging to any currently
8832 * open windows will get a new window.
8834 static void
8835 conv_placement_by_group(PidginConversation *conv)
8837 PurpleGroup *group = NULL;
8838 GList *wl, *cl;
8840 group = conv_get_group(conv);
8842 /* Go through the list of IMs and find one with this group. */
8843 for (wl = pidgin_conv_windows_get_list(); wl != NULL; wl = wl->next) {
8844 PidginConvWindow *win2;
8845 PidginConversation *conv2;
8846 PurpleGroup *group2 = NULL;
8848 win2 = wl->data;
8850 for (cl = win2->gtkconvs;
8851 cl != NULL;
8852 cl = cl->next) {
8853 conv2 = cl->data;
8855 group2 = conv_get_group(conv2);
8857 if (group == group2) {
8858 pidgin_conv_window_add_gtkconv(win2, conv);
8860 return;
8865 /* Make a new window. */
8866 conv_placement_new_window(conv);
8869 /* This groups things by account. Otherwise, the same semantics as above */
8870 static void
8871 conv_placement_by_account(PidginConversation *conv)
8873 GList *wins, *convs;
8874 PurpleAccount *account;
8876 account = purple_conversation_get_account(conv->active_conv);
8878 /* Go through the list of IMs and find one with this group. */
8879 for (wins = pidgin_conv_windows_get_list(); wins != NULL; wins = wins->next) {
8880 PidginConvWindow *win2;
8881 PidginConversation *conv2;
8883 win2 = wins->data;
8885 for (convs = win2->gtkconvs;
8886 convs != NULL;
8887 convs = convs->next) {
8888 conv2 = convs->data;
8890 if (account == purple_conversation_get_account(conv2->active_conv)) {
8891 pidgin_conv_window_add_gtkconv(win2, conv);
8892 return;
8897 /* Make a new window. */
8898 conv_placement_new_window(conv);
8901 static ConvPlacementData *
8902 get_conv_placement_data(const char *id)
8904 ConvPlacementData *data = NULL;
8905 GList *n;
8907 for (n = conv_placement_fncs; n; n = n->next) {
8908 data = n->data;
8909 if (purple_strequal(data->id, id))
8910 return data;
8913 return NULL;
8916 static void
8917 add_conv_placement_fnc(const char *id, const char *name,
8918 PidginConvPlacementFunc fnc)
8920 ConvPlacementData *data;
8922 data = g_new(ConvPlacementData, 1);
8924 data->id = g_strdup(id);
8925 data->name = g_strdup(name);
8926 data->fnc = fnc;
8928 conv_placement_fncs = g_list_append(conv_placement_fncs, data);
8931 static void
8932 ensure_default_funcs(void)
8934 if (conv_placement_fncs == NULL) {
8935 add_conv_placement_fnc("last", _("Last created window"),
8936 conv_placement_last_created_win);
8937 add_conv_placement_fnc("im_chat", _("Separate IM and Chat windows"),
8938 conv_placement_last_created_win_type);
8939 add_conv_placement_fnc("new", _("New window"),
8940 conv_placement_new_window);
8941 add_conv_placement_fnc("group", _("By group"),
8942 conv_placement_by_group);
8943 add_conv_placement_fnc("account", _("By account"),
8944 conv_placement_by_account);
8948 GList *
8949 pidgin_conv_placement_get_options(void)
8951 GList *n, *list = NULL;
8952 ConvPlacementData *data;
8954 ensure_default_funcs();
8956 for (n = conv_placement_fncs; n; n = n->next) {
8957 data = n->data;
8958 list = g_list_append(list, data->name);
8959 list = g_list_append(list, data->id);
8962 return list;
8966 void
8967 pidgin_conv_placement_add_fnc(const char *id, const char *name,
8968 PidginConvPlacementFunc fnc)
8970 g_return_if_fail(id != NULL);
8971 g_return_if_fail(name != NULL);
8972 g_return_if_fail(fnc != NULL);
8974 ensure_default_funcs();
8976 add_conv_placement_fnc(id, name, fnc);
8979 void
8980 pidgin_conv_placement_remove_fnc(const char *id)
8982 ConvPlacementData *data = get_conv_placement_data(id);
8984 if (data == NULL)
8985 return;
8987 conv_placement_fncs = g_list_remove(conv_placement_fncs, data);
8989 g_free(data->id);
8990 g_free(data->name);
8991 g_free(data);
8994 const char *
8995 pidgin_conv_placement_get_name(const char *id)
8997 ConvPlacementData *data;
8999 ensure_default_funcs();
9001 data = get_conv_placement_data(id);
9003 if (data == NULL)
9004 return NULL;
9006 return data->name;
9009 PidginConvPlacementFunc
9010 pidgin_conv_placement_get_fnc(const char *id)
9012 ConvPlacementData *data;
9014 ensure_default_funcs();
9016 data = get_conv_placement_data(id);
9018 if (data == NULL)
9019 return NULL;
9021 return data->fnc;
9024 void
9025 pidgin_conv_placement_set_current_func(PidginConvPlacementFunc func)
9027 g_return_if_fail(func != NULL);
9029 /* If tabs are enabled, set the function, otherwise, NULL it out. */
9030 if (purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/tabs"))
9031 place_conv = func;
9032 else
9033 place_conv = NULL;
9036 PidginConvPlacementFunc
9037 pidgin_conv_placement_get_current_func(void)
9039 return place_conv;
9042 void
9043 pidgin_conv_placement_place(PidginConversation *gtkconv)
9045 if (place_conv)
9046 place_conv(gtkconv);
9047 else
9048 conv_placement_new_window(gtkconv);
9051 gboolean
9052 pidgin_conv_is_hidden(PidginConversation *gtkconv)
9054 g_return_val_if_fail(gtkconv != NULL, FALSE);
9056 return (gtkconv->win == hidden_convwin);
9060 gdouble luminance(GdkRGBA color)
9062 gdouble r, g, b;
9063 gdouble rr, gg, bb;
9064 gdouble cutoff = 0.03928, scale = 12.92;
9065 gdouble a = 0.055, d = 1.055, p = 2.2;
9067 rr = color.red;
9068 gg = color.green;
9069 bb = color.blue;
9071 r = (rr > cutoff) ? pow((rr+a)/d, p) : rr/scale;
9072 g = (gg > cutoff) ? pow((gg+a)/d, p) : gg/scale;
9073 b = (bb > cutoff) ? pow((bb+a)/d, p) : bb/scale;
9075 return (r*0.2126 + g*0.7152 + b*0.0722);
9078 /* Algorithm from https://www.w3.org/TR/2008/REC-WCAG20-20081211/relative-luminance.xml */
9079 static gboolean
9080 color_is_visible(GdkRGBA foreground, GdkRGBA background, gdouble min_contrast_ratio)
9082 gdouble lfg, lbg, lmin, lmax;
9083 gdouble luminosity_ratio;
9084 gdouble nr, dr;
9086 lfg = luminance(foreground);
9087 lbg = luminance(background);
9089 if (lfg > lbg)
9090 lmax = lfg, lmin = lbg;
9091 else
9092 lmax = lbg, lmin = lfg;
9094 nr = lmax + 0.05, dr = lmin - 0.05;
9095 if (dr < 0.005 && dr > -0.005)
9096 dr += 0.01;
9098 luminosity_ratio = nr/dr;
9099 if ( luminosity_ratio < 0)
9100 luminosity_ratio *= -1.0;
9101 return (luminosity_ratio > min_contrast_ratio);
9105 static GArray*
9106 generate_nick_colors(guint numcolors, GdkRGBA background)
9108 guint i = 0, j = 0;
9109 GArray *colors = g_array_new(FALSE, FALSE, sizeof(GdkRGBA));
9110 GdkRGBA nick_highlight;
9111 GdkRGBA send_color;
9112 time_t breakout_time;
9114 gdk_rgba_parse(&nick_highlight, DEFAULT_HIGHLIGHT_COLOR);
9115 gdk_rgba_parse(&send_color, DEFAULT_SEND_COLOR);
9117 pidgin_style_adjust_contrast(NULL, &nick_highlight);
9118 pidgin_style_adjust_contrast(NULL, &send_color);
9120 srand(background.red * 65535 + background.green * 65535 + background.blue * 65535 + 1);
9122 breakout_time = time(NULL) + 3;
9124 /* first we look through the list of "good" colors: colors that differ from every other color in the
9125 * list. only some of them will differ from the background color though. lets see if we can find
9126 * numcolors of them that do
9128 while (i < numcolors && j < PIDGIN_NUM_NICK_SEED_COLORS && time(NULL) < breakout_time)
9130 GdkRGBA color = nick_seed_colors[j];
9132 if (color_is_visible(color, background, MIN_LUMINANCE_CONTRAST_RATIO) &&
9133 color_is_visible(color, nick_highlight, MIN_LUMINANCE_CONTRAST_RATIO) &&
9134 color_is_visible(color, send_color, MIN_LUMINANCE_CONTRAST_RATIO))
9136 g_array_append_val(colors, color);
9137 i++;
9139 j++;
9142 /* we might not have found numcolors in the last loop. if we did, we'll never enter this one.
9143 * if we did not, lets just find some colors that don't conflict with the background. its
9144 * expensive to find colors that not only don't conflict with the background, but also do not
9145 * conflict with each other.
9147 while(i < numcolors && time(NULL) < breakout_time)
9149 GdkRGBA color = {g_random_double_range(0, 1), g_random_double_range(0, 1), g_random_double_range(0, 1), 1};
9151 if (color_is_visible(color, background, MIN_LUMINANCE_CONTRAST_RATIO) &&
9152 color_is_visible(color, nick_highlight, MIN_LUMINANCE_CONTRAST_RATIO) &&
9153 color_is_visible(color, send_color, MIN_LUMINANCE_CONTRAST_RATIO))
9155 g_array_append_val(colors, color);
9156 i++;
9160 if (i < numcolors) {
9161 purple_debug_warning("gtkconv", "Unable to generate enough random colors before timeout. %u colors found.\n", i);
9164 if( i == 0 ) {
9165 /* To remove errors caused by an empty array. */
9166 GdkRGBA color = {0.5, 0.5, 0.5, 1.0};
9167 g_array_append_val(colors, color);
9170 return colors;
9173 /**************************************************************************
9174 * PidginConvWindow GBoxed code
9175 **************************************************************************/
9176 static PidginConvWindow *
9177 pidgin_conv_window_ref(PidginConvWindow *win)
9179 g_return_val_if_fail(win != NULL, NULL);
9181 win->box_count++;
9183 return win;
9186 static void
9187 pidgin_conv_window_unref(PidginConvWindow *win)
9189 g_return_if_fail(win != NULL);
9190 g_return_if_fail(win->box_count >= 0);
9192 if (!win->box_count--)
9193 pidgin_conv_window_destroy(win);
9196 GType
9197 pidgin_conv_window_get_type(void)
9199 static GType type = 0;
9201 if (type == 0) {
9202 type = g_boxed_type_register_static("PidginConvWindow",
9203 (GBoxedCopyFunc)pidgin_conv_window_ref,
9204 (GBoxedFreeFunc)pidgin_conv_window_unref);
9207 return type;