Updated German help translation
[empathy/ppotvin.git] / libempathy-gtk / empathy-chat.c
blobdec4757dbb2f0bec5d001dd8849f06a658dd4853
1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
3 * Copyright (C) 2002-2007 Imendio AB
4 * Copyright (C) 2007-2008 Collabora Ltd.
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License as
8 * published by the Free Software Foundation; either version 2 of the
9 * License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * General Public License for more details.
16 * You should have received a copy of the GNU General Public
17 * License along with this program; if not, write to the
18 * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
19 * Boston, MA 02110-1301 USA
21 * Authors: Mikael Hallendal <micke@imendio.com>
22 * Richard Hult <richard@imendio.com>
23 * Martyn Russell <martyn@imendio.com>
24 * Geert-Jan Van den Bogaerde <geertjan@gnome.org>
25 * Xavier Claessens <xclaesse@gmail.com>
28 #include <config.h>
30 #include <string.h>
31 #include <stdlib.h>
33 #include <gdk/gdkkeysyms.h>
34 #include <glib/gi18n-lib.h>
35 #include <gtk/gtk.h>
37 #include <telepathy-glib/account-manager.h>
38 #include <telepathy-glib/util.h>
40 #include <libempathy/empathy-log-manager.h>
41 #include <libempathy/empathy-contact-list.h>
42 #include <libempathy/empathy-utils.h>
43 #include <libempathy/empathy-dispatcher.h>
45 #include "empathy-chat.h"
46 #include "empathy-conf.h"
47 #include "empathy-spell.h"
48 #include "empathy-contact-list-store.h"
49 #include "empathy-contact-list-view.h"
50 #include "empathy-contact-menu.h"
51 #include "empathy-theme-manager.h"
52 #include "empathy-smiley-manager.h"
53 #include "empathy-ui-utils.h"
55 #define DEBUG_FLAG EMPATHY_DEBUG_CHAT
56 #include <libempathy/empathy-debug.h>
58 #define CHAT_DIR_CREATE_MODE (S_IRUSR | S_IWUSR | S_IXUSR)
59 #define CHAT_FILE_CREATE_MODE (S_IRUSR | S_IWUSR)
60 #define IS_ENTER(v) (v == GDK_Return || v == GDK_ISO_Enter || v == GDK_KP_Enter)
61 #define MAX_INPUT_HEIGHT 150
62 #define COMPOSING_STOP_TIMEOUT 5
64 #define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyChat)
65 typedef struct {
66 EmpathyTpChat *tp_chat;
67 TpAccount *account;
68 gchar *id;
69 gchar *name;
70 gchar *subject;
71 EmpathyContact *remote_contact;
72 gboolean show_contacts;
74 EmpathyLogManager *log_manager;
75 TpAccountManager *account_manager;
76 GList *input_history;
77 GList *input_history_current;
78 GList *compositors;
79 GCompletion *completion;
80 guint composing_stop_timeout_id;
81 guint block_events_timeout_id;
82 TpHandleType handle_type;
83 gint contacts_width;
84 gboolean has_input_vscroll;
86 GtkWidget *widget;
87 GtkWidget *hpaned;
88 GtkWidget *vbox_left;
89 GtkWidget *scrolled_window_chat;
90 GtkWidget *scrolled_window_input;
91 GtkWidget *scrolled_window_contacts;
92 GtkWidget *hbox_topic;
93 GtkWidget *label_topic;
94 GtkWidget *contact_list_view;
95 GtkWidget *info_bar_vbox;
97 guint unread_messages;
98 /* TRUE if the pending messages can be displayed. This is to avoid to show
99 * pending messages *before* messages from logs. (#603980) */
100 gboolean can_show_pending;
101 } EmpathyChatPriv;
103 typedef struct {
104 gchar *text; /* Original message that was specified
105 * upon entry creation. */
106 gchar *modified_text; /* Message that was modified by user.
107 * When no modifications were made, it is NULL */
108 } InputHistoryEntry;
110 enum {
111 COMPOSING,
112 NEW_MESSAGE,
113 LAST_SIGNAL
116 enum {
117 PROP_0,
118 PROP_TP_CHAT,
119 PROP_ACCOUNT,
120 PROP_ID,
121 PROP_NAME,
122 PROP_SUBJECT,
123 PROP_REMOTE_CONTACT,
124 PROP_SHOW_CONTACTS,
127 static guint signals[LAST_SIGNAL] = { 0 };
129 G_DEFINE_TYPE (EmpathyChat, empathy_chat, GTK_TYPE_BIN);
131 static void
132 chat_get_property (GObject *object,
133 guint param_id,
134 GValue *value,
135 GParamSpec *pspec)
137 EmpathyChat *chat = EMPATHY_CHAT (object);
138 EmpathyChatPriv *priv = GET_PRIV (object);
140 switch (param_id) {
141 case PROP_TP_CHAT:
142 g_value_set_object (value, priv->tp_chat);
143 break;
144 case PROP_ACCOUNT:
145 g_value_set_object (value, priv->account);
146 break;
147 case PROP_NAME:
148 g_value_set_string (value, empathy_chat_get_name (chat));
149 break;
150 case PROP_ID:
151 g_value_set_string (value, priv->id);
152 break;
153 case PROP_SUBJECT:
154 g_value_set_string (value, priv->subject);
155 break;
156 case PROP_REMOTE_CONTACT:
157 g_value_set_object (value, priv->remote_contact);
158 break;
159 case PROP_SHOW_CONTACTS:
160 g_value_set_boolean (value, priv->show_contacts);
161 break;
162 default:
163 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
164 break;
168 static void
169 chat_set_property (GObject *object,
170 guint param_id,
171 const GValue *value,
172 GParamSpec *pspec)
174 EmpathyChat *chat = EMPATHY_CHAT (object);
176 switch (param_id) {
177 case PROP_TP_CHAT:
178 empathy_chat_set_tp_chat (chat, EMPATHY_TP_CHAT (g_value_get_object (value)));
179 break;
180 case PROP_SHOW_CONTACTS:
181 empathy_chat_set_show_contacts (chat, g_value_get_boolean (value));
182 break;
183 default:
184 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
185 break;
189 static void
190 chat_connect_channel_reconnected (EmpathyDispatchOperation *dispatch,
191 const GError *error,
192 gpointer user_data)
194 EmpathyChat *chat = EMPATHY_CHAT (user_data);
195 EmpathyTpChat *tpchat;
197 if (error != NULL) {
198 empathy_chat_view_append_event (chat->view,
199 _("Failed to reconnect this chat"));
200 return;
203 tpchat = EMPATHY_TP_CHAT (
204 empathy_dispatch_operation_get_channel_wrapper (dispatch));
206 if (empathy_dispatch_operation_claim (dispatch)) {
207 empathy_chat_set_tp_chat (chat, tpchat);
211 static void
212 reconnected_connection_ready_cb (TpConnection *connection,
213 const GError *error,
214 gpointer user_data)
216 EmpathyChat *chat = user_data;
217 EmpathyChatPriv *priv = GET_PRIV (chat);
219 if (error != NULL) {
220 DEBUG ("connection is not ready: %s", error->message);
221 goto out;
224 DEBUG ("Account reconnected, request a new Text channel");
226 switch (priv->handle_type) {
227 case TP_HANDLE_TYPE_CONTACT:
228 empathy_dispatcher_chat_with_contact_id (
229 connection, priv->id,
230 chat_connect_channel_reconnected,
231 chat);
232 break;
233 case TP_HANDLE_TYPE_ROOM:
234 empathy_dispatcher_join_muc (connection,
235 priv->id,
236 chat_connect_channel_reconnected,
237 chat);
238 break;
239 default:
240 g_assert_not_reached ();
241 break;
244 out:
245 g_object_unref (chat);
248 static void
249 chat_new_connection_cb (TpAccount *account,
250 guint old_status,
251 guint new_status,
252 guint reason,
253 gchar *dbus_error_name,
254 GHashTable *details,
255 EmpathyChat *chat)
257 EmpathyChatPriv *priv = GET_PRIV (chat);
258 TpConnection *connection;
260 if (new_status != TP_CONNECTION_STATUS_CONNECTED)
261 return;
263 connection = tp_account_get_connection (account);
265 if (priv->tp_chat != NULL || account != priv->account ||
266 priv->handle_type == TP_HANDLE_TYPE_NONE ||
267 EMP_STR_EMPTY (priv->id))
268 return;
270 g_object_ref (chat);
271 tp_connection_call_when_ready (connection, reconnected_connection_ready_cb,
272 chat);
275 static void
276 chat_composing_remove_timeout (EmpathyChat *chat)
278 EmpathyChatPriv *priv;
280 priv = GET_PRIV (chat);
282 if (priv->composing_stop_timeout_id) {
283 g_source_remove (priv->composing_stop_timeout_id);
284 priv->composing_stop_timeout_id = 0;
288 static gboolean
289 chat_composing_stop_timeout_cb (EmpathyChat *chat)
291 EmpathyChatPriv *priv;
293 priv = GET_PRIV (chat);
295 priv->composing_stop_timeout_id = 0;
296 empathy_tp_chat_set_state (priv->tp_chat,
297 TP_CHANNEL_CHAT_STATE_PAUSED);
299 return FALSE;
302 static void
303 chat_composing_start (EmpathyChat *chat)
305 EmpathyChatPriv *priv;
307 priv = GET_PRIV (chat);
309 if (priv->composing_stop_timeout_id) {
310 /* Just restart the timeout */
311 chat_composing_remove_timeout (chat);
312 } else {
313 empathy_tp_chat_set_state (priv->tp_chat,
314 TP_CHANNEL_CHAT_STATE_COMPOSING);
317 priv->composing_stop_timeout_id = g_timeout_add_seconds (
318 COMPOSING_STOP_TIMEOUT,
319 (GSourceFunc) chat_composing_stop_timeout_cb,
320 chat);
323 static void
324 chat_composing_stop (EmpathyChat *chat)
326 EmpathyChatPriv *priv;
328 priv = GET_PRIV (chat);
330 chat_composing_remove_timeout (chat);
331 empathy_tp_chat_set_state (priv->tp_chat,
332 TP_CHANNEL_CHAT_STATE_ACTIVE);
335 static gint
336 chat_input_history_entry_cmp (InputHistoryEntry *entry,
337 const gchar *text)
339 if (!tp_strdiff (entry->text, text)) {
340 if (entry->modified_text != NULL) {
341 /* Modified entry and single string cannot be equal. */
342 return 1;
344 return 0;
346 return 1;
349 static InputHistoryEntry *
350 chat_input_history_entry_new_with_text (const gchar *text)
352 InputHistoryEntry *entry;
353 entry = g_slice_new0 (InputHistoryEntry);
354 entry->text = g_strdup (text);
356 return entry;
359 static void
360 chat_input_history_entry_free (InputHistoryEntry *entry)
362 g_free (entry->text);
363 g_free (entry->modified_text);
364 g_slice_free (InputHistoryEntry, entry);
367 static void
368 chat_input_history_entry_revert (InputHistoryEntry *entry)
370 g_free (entry->modified_text);
371 entry->modified_text = NULL;
374 static void
375 chat_input_history_entry_update_text (InputHistoryEntry *entry,
376 const gchar *text)
378 gchar *old;
380 if (!tp_strdiff (text, entry->text)) {
381 g_free (entry->modified_text);
382 entry->modified_text = NULL;
383 return;
386 old = entry->modified_text;
387 entry->modified_text = g_strdup (text);
388 g_free (old);
391 static const gchar *
392 chat_input_history_entry_get_text (InputHistoryEntry *entry)
394 if (entry == NULL) {
395 return NULL;
398 if (entry->modified_text != NULL) {
399 return entry->modified_text;
401 return entry->text;
404 static GList *
405 chat_input_history_remove_item (GList *list,
406 GList *item)
408 list = g_list_remove_link (list, item);
409 chat_input_history_entry_free (item->data);
410 g_list_free_1 (item);
411 return list;
414 static void
415 chat_input_history_revert (EmpathyChat *chat)
417 EmpathyChatPriv *priv;
418 GList *list;
419 GList *item1;
420 GList *item2;
421 InputHistoryEntry *entry;
423 priv = GET_PRIV (chat);
424 list = priv->input_history;
426 if (list == NULL) {
427 DEBUG ("No input history");
428 return;
431 /* Delete temporary entry */
432 if (priv->input_history_current != NULL) {
433 item1 = list;
434 list = chat_input_history_remove_item (list, item1);
435 if (priv->input_history_current == item1) {
436 /* Removed temporary entry was current entry */
437 priv->input_history = list;
438 priv->input_history_current = NULL;
439 return;
442 else {
443 /* There is no entry to revert */
444 return;
447 /* Restore the current history entry to original value */
448 item1 = priv->input_history_current;
449 entry = item1->data;
450 chat_input_history_entry_revert (entry);
452 /* Remove restored entry if there is other occurance before this entry */
453 item2 = g_list_find_custom (list, chat_input_history_entry_get_text (entry),
454 (GCompareFunc) chat_input_history_entry_cmp);
455 if (item2 != item1) {
456 list = chat_input_history_remove_item (list, item1);
458 else {
459 /* Remove other occurance of the restored entry */
460 item2 = g_list_find_custom (item1->next,
461 chat_input_history_entry_get_text (entry),
462 (GCompareFunc) chat_input_history_entry_cmp);
463 if (item2 != NULL) {
464 list = chat_input_history_remove_item (list, item2);
468 priv->input_history_current = NULL;
469 priv->input_history = list;
472 static void
473 chat_input_history_add (EmpathyChat *chat,
474 const gchar *str,
475 gboolean temporary)
477 EmpathyChatPriv *priv;
478 GList *list;
479 GList *item;
480 InputHistoryEntry *entry;
482 priv = GET_PRIV (chat);
484 list = priv->input_history;
486 /* Remove any other occurances of this entry, if not temporary */
487 if (!temporary) {
488 while ((item = g_list_find_custom (list, str,
489 (GCompareFunc) chat_input_history_entry_cmp)) != NULL) {
490 list = chat_input_history_remove_item (list, item);
493 /* Trim the list to the last 10 items */
494 while (g_list_length (list) > 10) {
495 item = g_list_last (list);
496 if (item != NULL) {
497 list = chat_input_history_remove_item (list, item);
504 /* Add new entry */
505 entry = chat_input_history_entry_new_with_text (str);
506 list = g_list_prepend (list, entry);
508 /* Set the list and the current item pointer */
509 priv->input_history = list;
510 if (temporary) {
511 priv->input_history_current = list;
513 else {
514 priv->input_history_current = NULL;
518 static const gchar *
519 chat_input_history_get_next (EmpathyChat *chat)
521 EmpathyChatPriv *priv;
522 GList *item;
523 const gchar *msg;
525 priv = GET_PRIV (chat);
527 if (priv->input_history == NULL) {
528 DEBUG ("No input history, next entry is NULL");
529 return NULL;
531 g_assert (priv->input_history_current != NULL);
533 if ((item = g_list_next (priv->input_history_current)) == NULL)
535 item = priv->input_history_current;
538 msg = chat_input_history_entry_get_text (item->data);
540 DEBUG ("Returning next entry: '%s'", msg);
542 priv->input_history_current = item;
544 return msg;
547 static const gchar *
548 chat_input_history_get_prev (EmpathyChat *chat)
550 EmpathyChatPriv *priv;
551 GList *item;
552 const gchar *msg;
554 g_return_val_if_fail (EMPATHY_IS_CHAT (chat), NULL);
556 priv = GET_PRIV (chat);
558 if (priv->input_history == NULL) {
559 DEBUG ("No input history, previous entry is NULL");
560 return NULL;
563 if (priv->input_history_current == NULL)
565 return NULL;
567 else if ((item = g_list_previous (priv->input_history_current)) == NULL)
569 item = priv->input_history_current;
572 msg = chat_input_history_entry_get_text (item->data);
574 DEBUG ("Returning previous entry: '%s'", msg);
576 priv->input_history_current = item;
578 return msg;
581 static void
582 chat_input_history_update (EmpathyChat *chat,
583 GtkTextBuffer *buffer)
585 EmpathyChatPriv *priv;
586 GtkTextIter start, end;
587 gchar *text;
588 InputHistoryEntry *entry;
590 priv = GET_PRIV (chat);
592 gtk_text_buffer_get_bounds (buffer, &start, &end);
593 text = gtk_text_buffer_get_text (buffer, &start, &end, FALSE);
595 if (priv->input_history_current == NULL) {
596 /* Add the current text temporarily to the history */
597 chat_input_history_add (chat, text, TRUE);
598 g_free (text);
599 return;
602 /* Save the changes in the history */
603 entry = priv->input_history_current->data;
604 if (tp_strdiff (chat_input_history_entry_get_text (entry), text)) {
605 chat_input_history_entry_update_text (entry, text);
608 g_free (text);
611 static void
612 chat_command_join_cb (EmpathyDispatchOperation *dispatch,
613 const GError *error,
614 gpointer user_data)
616 EmpathyChat *chat = user_data;
618 if (error != NULL) {
619 DEBUG ("Error: %s", error->message);
620 empathy_chat_view_append_event (chat->view,
621 _("Failed to join chatroom"));
625 typedef struct {
626 EmpathyChat *chat;
627 gchar *message;
628 } ChatCommandMsgData;
630 static void
631 chat_command_msg_cb (EmpathyDispatchOperation *dispatch,
632 const GError *error,
633 gpointer user_data)
635 ChatCommandMsgData *data = user_data;
637 if (error != NULL) {
638 empathy_chat_view_append_event (data->chat->view,
639 _("Failed to open private chat"));
640 goto OUT;
643 if (!EMP_STR_EMPTY (data->message)) {
644 EmpathyTpChat *tpchat;
645 EmpathyMessage *message;
647 tpchat = EMPATHY_TP_CHAT (
648 empathy_dispatch_operation_get_channel_wrapper (dispatch));
650 message = empathy_message_new (data->message);
651 empathy_tp_chat_send (tpchat, message);
652 g_object_unref (message);
655 OUT:
656 g_free (data->message);
657 g_slice_free (ChatCommandMsgData, data);
660 static void
661 chat_command_clear (EmpathyChat *chat,
662 GStrv strv)
664 empathy_chat_view_clear (chat->view);
667 static void
668 chat_command_topic (EmpathyChat *chat,
669 GStrv strv)
671 EmpathyChatPriv *priv = GET_PRIV (chat);
672 EmpathyTpChatProperty *property;
673 GValue value = {0, };
675 property = empathy_tp_chat_get_property (priv->tp_chat, "subject");
676 if (property == NULL) {
677 empathy_chat_view_append_event (chat->view,
678 _("Topic not supported on this conversation"));
679 return;
682 if (!(property->flags & TP_PROPERTY_FLAG_WRITE)) {
683 empathy_chat_view_append_event (chat->view,
684 _("You are not allowed to change the topic"));
685 return;
688 g_value_init (&value, G_TYPE_STRING);
689 g_value_set_string (&value, strv[1]);
690 empathy_tp_chat_set_property (priv->tp_chat, "subject", &value);
691 g_value_unset (&value);
694 static void
695 chat_command_join (EmpathyChat *chat,
696 GStrv strv)
698 EmpathyChatPriv *priv = GET_PRIV (chat);
699 TpConnection *connection;
701 connection = empathy_tp_chat_get_connection (priv->tp_chat);
702 empathy_dispatcher_join_muc (connection, strv[1],
703 chat_command_join_cb,
704 chat);
707 static void
708 chat_command_msg_internal (EmpathyChat *chat,
709 const gchar *contact_id,
710 const gchar *message)
712 EmpathyChatPriv *priv = GET_PRIV (chat);
713 TpConnection *connection;
714 ChatCommandMsgData *data;
716 /* FIXME: We should probably search in members alias. But this
717 * is enough for IRC */
718 data = g_slice_new (ChatCommandMsgData);
719 data->chat = chat;
720 data->message = g_strdup (message);
721 connection = empathy_tp_chat_get_connection (priv->tp_chat);
722 empathy_dispatcher_chat_with_contact_id (connection, contact_id,
723 chat_command_msg_cb,
724 data);
727 static void
728 chat_command_query (EmpathyChat *chat,
729 GStrv strv)
731 /* If <message> part is not defined,
732 * strv[2] will be the terminal NULL */
733 chat_command_msg_internal (chat, strv[1], strv[2]);
736 static void
737 chat_command_msg (EmpathyChat *chat,
738 GStrv strv)
740 chat_command_msg_internal (chat, strv[1], strv[2]);
743 static void
744 chat_command_nick (EmpathyChat *chat,
745 GStrv strv)
747 EmpathyChatPriv *priv = GET_PRIV (chat);
748 TpConnection *connection;
749 GHashTable *new_alias;
750 TpHandle handle;
752 connection = tp_account_get_connection (priv->account);
753 handle = tp_connection_get_self_handle (connection);
754 new_alias = g_hash_table_new (g_direct_hash, g_direct_equal);
755 g_hash_table_insert (new_alias, GUINT_TO_POINTER (handle), strv[1]);
757 tp_cli_connection_interface_aliasing_call_set_aliases (connection, -1,
758 new_alias, NULL, NULL, NULL, NULL);
760 g_hash_table_destroy (new_alias);
763 static void
764 chat_command_me (EmpathyChat *chat,
765 GStrv strv)
767 EmpathyChatPriv *priv = GET_PRIV (chat);
768 EmpathyMessage *message;
770 message = empathy_message_new (strv[1]);
771 empathy_message_set_tptype (message, TP_CHANNEL_TEXT_MESSAGE_TYPE_ACTION);
772 empathy_tp_chat_send (priv->tp_chat, message);
773 g_object_unref (message);
776 static void
777 chat_command_say (EmpathyChat *chat,
778 GStrv strv)
780 EmpathyChatPriv *priv = GET_PRIV (chat);
781 EmpathyMessage *message;
783 message = empathy_message_new (strv[1]);
784 empathy_tp_chat_send (priv->tp_chat, message);
785 g_object_unref (message);
788 static void chat_command_help (EmpathyChat *chat, GStrv strv);
790 typedef void (*ChatCommandFunc) (EmpathyChat *chat, GStrv strv);
792 typedef struct {
793 const gchar *prefix;
794 guint min_parts;
795 guint max_parts;
796 ChatCommandFunc func;
797 const gchar *help;
798 } ChatCommandItem;
800 static ChatCommandItem commands[] = {
801 {"clear", 1, 1, chat_command_clear,
802 N_("/clear, clear all messages from the current conversation")},
804 {"topic", 2, 2, chat_command_topic,
805 N_("/topic <topic>, set the topic of the current conversation")},
807 {"join", 2, 2, chat_command_join,
808 N_("/join <chatroom id>, join a new chatroom")},
810 {"j", 2, 2, chat_command_join,
811 N_("/j <chatroom id>, join a new chatroom")},
813 {"query", 2, 3, chat_command_query,
814 N_("/query <contact id> [<message>], open a private chat")},
816 {"msg", 3, 3, chat_command_msg,
817 N_("/msg <contact id> <message>, open a private chat")},
819 {"nick", 2, 2, chat_command_nick,
820 N_("/nick <nickname>, change your nickname on current server")},
822 {"me", 2, 2, chat_command_me,
823 N_("/me <message>, send an ACTION message to the current conversation")},
825 {"say", 2, 2, chat_command_say,
826 N_("/say <message>, send <message> to the current conversation. "
827 "This is used to send a message starting with a '/'. For example: "
828 "\"/say /join is used to join a new chatroom\"")},
830 {"help", 1, 2, chat_command_help,
831 N_("/help [<command>], show all supported commands. "
832 "If <command> is defined, show its usage.")},
835 static void
836 chat_command_show_help (EmpathyChat *chat,
837 ChatCommandItem *item)
839 gchar *str;
841 str = g_strdup_printf (_("Usage: %s"), _(item->help));
842 empathy_chat_view_append_event (chat->view, str);
843 g_free (str);
846 static void
847 chat_command_help (EmpathyChat *chat,
848 GStrv strv)
850 guint i;
852 /* If <command> part is not defined,
853 * strv[1] will be the terminal NULL */
854 if (strv[1] == NULL) {
855 for (i = 0; i < G_N_ELEMENTS (commands); i++) {
856 empathy_chat_view_append_event (chat->view,
857 _(commands[i].help));
859 return;
862 for (i = 0; i < G_N_ELEMENTS (commands); i++) {
863 if (g_ascii_strcasecmp (strv[1], commands[i].prefix) == 0) {
864 chat_command_show_help (chat, &commands[i]);
865 return;
869 empathy_chat_view_append_event (chat->view,
870 _("Unknown command"));
873 static GStrv
874 chat_command_parse (const gchar *text, guint max_parts)
876 GPtrArray *array;
877 gchar *item;
879 DEBUG ("Parse command, parts=%d text=\"%s\":", max_parts, text);
881 array = g_ptr_array_sized_new (max_parts + 1);
882 while (max_parts > 1) {
883 const gchar *end;
885 /* Skip white spaces */
886 while (g_ascii_isspace (*text)) {
887 text++;
890 /* Search the end of this part, until first space. */
891 for (end = text; *end != '\0' && !g_ascii_isspace (*end); end++)
892 /* Do nothing */;
893 if (*end == '\0') {
894 break;
897 item = g_strndup (text, end - text);
898 g_ptr_array_add (array, item);
899 DEBUG ("\tITEM: \"%s\"", item);
901 text = end;
902 max_parts--;
905 /* Append last part if not empty */
906 item = g_strstrip (g_strdup (text));
907 if (!EMP_STR_EMPTY (item)) {
908 g_ptr_array_add (array, item);
909 DEBUG ("\tITEM: \"%s\"", item);
910 } else {
911 g_free (item);
914 /* Make the array NULL-terminated */
915 g_ptr_array_add (array, NULL);
917 return (GStrv) g_ptr_array_free (array, FALSE);
920 static gboolean
921 has_prefix_case (const gchar *s,
922 const gchar *prefix)
924 return g_ascii_strncasecmp (s, prefix, strlen (prefix)) == 0;
927 static void
928 chat_send (EmpathyChat *chat,
929 const gchar *msg)
931 EmpathyChatPriv *priv;
932 EmpathyMessage *message;
933 guint i;
935 if (EMP_STR_EMPTY (msg)) {
936 return;
939 priv = GET_PRIV (chat);
941 chat_input_history_add (chat, msg, FALSE);
943 if (msg[0] == '/') {
944 gboolean second_slash = FALSE;
945 const gchar *iter = msg + 1;
947 for (i = 0; i < G_N_ELEMENTS (commands); i++) {
948 GStrv strv;
949 guint strv_len;
950 gchar c;
952 if (!has_prefix_case (msg + 1, commands[i].prefix)) {
953 continue;
955 c = *(msg + 1 + strlen (commands[i].prefix));
956 if (c != '\0' && !g_ascii_isspace (c)) {
957 continue;
960 /* We can't use g_strsplit here because it does
961 * not deal correctly if we have more than one space
962 * between args */
963 strv = chat_command_parse (msg + 1, commands[i].max_parts);
965 strv_len = g_strv_length (strv);
966 if (strv_len < commands[i].min_parts ||
967 strv_len > commands[i].max_parts) {
968 chat_command_show_help (chat, &commands[i]);
969 g_strfreev (strv);
970 return;
973 commands[i].func (chat, strv);
974 g_strfreev (strv);
975 return;
978 /* Also allow messages with two slashes before the
979 * first space, so it is possible to send a /unix/path.
980 * This heuristic is kind of crap. */
981 while (*iter != '\0' && !g_ascii_isspace (*iter)) {
982 if (*iter == '/') {
983 second_slash = TRUE;
984 break;
986 iter++;
989 if (!second_slash) {
990 empathy_chat_view_append_event (chat->view,
991 _("Unknown command, see /help for the available"
992 " commands"));
993 return;
997 message = empathy_message_new (msg);
998 empathy_tp_chat_send (priv->tp_chat, message);
999 g_object_unref (message);
1002 static void
1003 chat_input_text_view_send (EmpathyChat *chat)
1005 EmpathyChatPriv *priv;
1006 GtkTextBuffer *buffer;
1007 GtkTextIter start, end;
1008 gchar *msg;
1010 priv = GET_PRIV (chat);
1012 buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (chat->input_text_view));
1014 gtk_text_buffer_get_bounds (buffer, &start, &end);
1015 msg = gtk_text_buffer_get_text (buffer, &start, &end, FALSE);
1017 /* clear the input field */
1018 gtk_text_buffer_set_text (buffer, "", -1);
1019 /* delete input history modifications */
1020 chat_input_history_revert (chat);
1022 chat_send (chat, msg);
1023 g_free (msg);
1026 static void
1027 chat_state_changed_cb (EmpathyTpChat *tp_chat,
1028 EmpathyContact *contact,
1029 TpChannelChatState state,
1030 EmpathyChat *chat)
1032 EmpathyChatPriv *priv;
1033 GList *l;
1034 gboolean was_composing;
1036 priv = GET_PRIV (chat);
1038 if (empathy_contact_is_user (contact)) {
1039 /* We don't care about our own chat state */
1040 return;
1043 was_composing = (priv->compositors != NULL);
1045 /* Find the contact in the list. After that l is the list elem or NULL */
1046 for (l = priv->compositors; l; l = l->next) {
1047 if (contact == l->data) {
1048 break;
1052 switch (state) {
1053 case TP_CHANNEL_CHAT_STATE_GONE:
1054 case TP_CHANNEL_CHAT_STATE_INACTIVE:
1055 case TP_CHANNEL_CHAT_STATE_PAUSED:
1056 case TP_CHANNEL_CHAT_STATE_ACTIVE:
1057 /* Contact is not composing */
1058 if (l) {
1059 priv->compositors = g_list_remove_link (priv->compositors, l);
1060 g_object_unref (l->data);
1061 g_list_free1 (l);
1063 break;
1064 case TP_CHANNEL_CHAT_STATE_COMPOSING:
1065 /* Contact is composing */
1066 if (!l) {
1067 priv->compositors = g_list_prepend (priv->compositors,
1068 g_object_ref (contact));
1070 break;
1071 default:
1072 g_assert_not_reached ();
1075 DEBUG ("Was composing: %s now composing: %s",
1076 was_composing ? "yes" : "no",
1077 priv->compositors ? "yes" : "no");
1079 if ((was_composing && !priv->compositors) ||
1080 (!was_composing && priv->compositors)) {
1081 /* Composing state changed */
1082 g_signal_emit (chat, signals[COMPOSING], 0,
1083 priv->compositors != NULL);
1087 static void
1088 chat_message_received (EmpathyChat *chat, EmpathyMessage *message)
1090 EmpathyChatPriv *priv = GET_PRIV (chat);
1091 EmpathyContact *sender;
1093 sender = empathy_message_get_sender (message);
1095 DEBUG ("Appending new message from %s (%d)",
1096 empathy_contact_get_name (sender),
1097 empathy_contact_get_handle (sender));
1099 empathy_chat_view_append_message (chat->view, message);
1101 /* We received a message so the contact is no longer composing */
1102 chat_state_changed_cb (priv->tp_chat, sender,
1103 TP_CHANNEL_CHAT_STATE_ACTIVE,
1104 chat);
1106 priv->unread_messages++;
1107 g_signal_emit (chat, signals[NEW_MESSAGE], 0, message);
1110 static void
1111 chat_message_received_cb (EmpathyTpChat *tp_chat,
1112 EmpathyMessage *message,
1113 EmpathyChat *chat)
1115 chat_message_received (chat, message);
1116 empathy_tp_chat_acknowledge_message (tp_chat, message);
1119 static void
1120 chat_send_error_cb (EmpathyTpChat *tp_chat,
1121 const gchar *message_body,
1122 TpChannelTextSendError error_code,
1123 EmpathyChat *chat)
1125 const gchar *error;
1126 gchar *str;
1128 switch (error_code) {
1129 case TP_CHANNEL_TEXT_SEND_ERROR_OFFLINE:
1130 error = _("offline");
1131 break;
1132 case TP_CHANNEL_TEXT_SEND_ERROR_INVALID_CONTACT:
1133 error = _("invalid contact");
1134 break;
1135 case TP_CHANNEL_TEXT_SEND_ERROR_PERMISSION_DENIED:
1136 error = _("permission denied");
1137 break;
1138 case TP_CHANNEL_TEXT_SEND_ERROR_TOO_LONG:
1139 error = _("too long message");
1140 break;
1141 case TP_CHANNEL_TEXT_SEND_ERROR_NOT_IMPLEMENTED:
1142 error = _("not implemented");
1143 break;
1144 default:
1145 error = _("unknown");
1146 break;
1149 str = g_strdup_printf (_("Error sending message '%s': %s"),
1150 message_body,
1151 error);
1152 empathy_chat_view_append_event (chat->view, str);
1153 g_free (str);
1156 static void
1157 chat_property_changed_cb (EmpathyTpChat *tp_chat,
1158 const gchar *name,
1159 GValue *value,
1160 EmpathyChat *chat)
1162 EmpathyChatPriv *priv = GET_PRIV (chat);
1164 if (!tp_strdiff (name, "subject")) {
1165 g_free (priv->subject);
1166 priv->subject = g_value_dup_string (value);
1167 g_object_notify (G_OBJECT (chat), "subject");
1169 if (EMP_STR_EMPTY (priv->subject)) {
1170 gtk_widget_hide (priv->hbox_topic);
1171 } else {
1172 gtk_label_set_text (GTK_LABEL (priv->label_topic), priv->subject);
1173 gtk_widget_show (priv->hbox_topic);
1175 if (priv->block_events_timeout_id == 0) {
1176 gchar *str;
1178 if (!EMP_STR_EMPTY (priv->subject)) {
1179 str = g_strdup_printf (_("Topic set to: %s"), priv->subject);
1180 } else {
1181 str = g_strdup (_("No topic defined"));
1183 empathy_chat_view_append_event (EMPATHY_CHAT (chat)->view, str);
1184 g_free (str);
1187 else if (!tp_strdiff (name, "name")) {
1188 g_free (priv->name);
1189 priv->name = g_value_dup_string (value);
1190 g_object_notify (G_OBJECT (chat), "name");
1194 static void
1195 chat_input_text_buffer_changed_cb (GtkTextBuffer *buffer,
1196 EmpathyChat *chat)
1198 EmpathyChatPriv *priv;
1199 GtkTextIter start, end;
1200 gchar *str;
1201 gboolean spell_checker = FALSE;
1203 priv = GET_PRIV (chat);
1205 if (gtk_text_buffer_get_char_count (buffer) == 0) {
1206 chat_composing_stop (chat);
1207 } else {
1208 chat_composing_start (chat);
1211 empathy_conf_get_bool (empathy_conf_get (),
1212 EMPATHY_PREFS_CHAT_SPELL_CHECKER_ENABLED,
1213 &spell_checker);
1215 gtk_text_buffer_get_start_iter (buffer, &start);
1217 if (!spell_checker) {
1218 gtk_text_buffer_get_end_iter (buffer, &end);
1219 gtk_text_buffer_remove_tag_by_name (buffer, "misspelled", &start, &end);
1220 return;
1223 if (!empathy_spell_supported ()) {
1224 return;
1227 /* NOTE: this is really inefficient, we shouldn't have to
1228 reiterate the whole buffer each time and check each work
1229 every time. */
1230 while (TRUE) {
1231 gboolean correct = FALSE;
1233 /* if at start */
1234 if (gtk_text_iter_is_start (&start)) {
1235 end = start;
1237 if (!gtk_text_iter_forward_word_end (&end)) {
1238 /* no whole word yet */
1239 break;
1241 } else {
1242 if (!gtk_text_iter_forward_word_end (&end)) {
1243 /* must be the end of the buffer */
1244 break;
1247 start = end;
1248 gtk_text_iter_backward_word_start (&start);
1251 str = gtk_text_buffer_get_text (buffer, &start, &end, FALSE);
1253 /* spell check string if not a command */
1254 if (str[0] != '/') {
1255 correct = empathy_spell_check (str);
1256 } else {
1257 correct = TRUE;
1260 if (!correct) {
1261 gtk_text_buffer_apply_tag_by_name (buffer, "misspelled", &start, &end);
1262 } else {
1263 gtk_text_buffer_remove_tag_by_name (buffer, "misspelled", &start, &end);
1266 g_free (str);
1268 /* set start iter to the end iters position */
1269 start = end;
1273 static gboolean
1274 chat_input_key_press_event_cb (GtkWidget *widget,
1275 GdkEventKey *event,
1276 EmpathyChat *chat)
1278 EmpathyChatPriv *priv;
1279 GtkAdjustment *adj;
1280 gdouble val;
1281 GtkWidget *text_view_sw;
1283 priv = GET_PRIV (chat);
1285 /* Catch ctrl+up/down so we can traverse messages we sent */
1286 if ((event->state & GDK_CONTROL_MASK) &&
1287 (event->keyval == GDK_Up ||
1288 event->keyval == GDK_Down)) {
1289 GtkTextBuffer *buffer;
1290 const gchar *str;
1292 buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (chat->input_text_view));
1293 chat_input_history_update (chat, buffer);
1295 if (event->keyval == GDK_Up) {
1296 str = chat_input_history_get_next (chat);
1297 } else {
1298 str = chat_input_history_get_prev (chat);
1301 g_signal_handlers_block_by_func (buffer,
1302 chat_input_text_buffer_changed_cb,
1303 chat);
1304 gtk_text_buffer_set_text (buffer, str ? str : "", -1);
1305 g_signal_handlers_unblock_by_func (buffer,
1306 chat_input_text_buffer_changed_cb,
1307 chat);
1309 return TRUE;
1312 /* Catch enter but not ctrl/shift-enter */
1313 if (IS_ENTER (event->keyval) &&
1314 !(event->state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK))) {
1315 GtkTextView *view;
1317 /* This is to make sure that kinput2 gets the enter. And if
1318 * it's handled there we shouldn't send on it. This is because
1319 * kinput2 uses Enter to commit letters. See:
1320 * http://bugzilla.redhat.com/bugzilla/show_bug.cgi?id=104299
1323 view = GTK_TEXT_VIEW (chat->input_text_view);
1324 if (gtk_im_context_filter_keypress (view->im_context, event)) {
1325 GTK_TEXT_VIEW (chat->input_text_view)->need_im_reset = TRUE;
1326 return TRUE;
1329 chat_input_text_view_send (chat);
1330 return TRUE;
1333 text_view_sw = gtk_widget_get_parent (GTK_WIDGET (chat->view));
1335 if (IS_ENTER (event->keyval) &&
1336 (event->state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK))) {
1337 /* Newline for shift/control-enter. */
1338 return FALSE;
1340 if (!(event->state & GDK_CONTROL_MASK) &&
1341 event->keyval == GDK_Page_Up) {
1342 adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (text_view_sw));
1343 gtk_adjustment_set_value (adj, gtk_adjustment_get_value (adj) - gtk_adjustment_get_page_size (adj));
1344 return TRUE;
1346 if ((event->state & GDK_CONTROL_MASK) != GDK_CONTROL_MASK &&
1347 event->keyval == GDK_Page_Down) {
1348 adj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (text_view_sw));
1349 val = MIN (gtk_adjustment_get_value (adj) + gtk_adjustment_get_page_size (adj),
1350 gtk_adjustment_get_upper (adj) - gtk_adjustment_get_page_size (adj));
1351 gtk_adjustment_set_value (adj, val);
1352 return TRUE;
1354 if (!(event->state & (GDK_CONTROL_MASK | GDK_SHIFT_MASK)) &&
1355 event->keyval == GDK_Tab) {
1356 GtkTextBuffer *buffer;
1357 GtkTextIter start, current;
1358 gchar *nick, *completed;
1359 GList *list, *completed_list;
1360 gboolean is_start_of_buffer;
1362 buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (EMPATHY_CHAT (chat)->input_text_view));
1363 gtk_text_buffer_get_iter_at_mark (buffer, &current, gtk_text_buffer_get_insert (buffer));
1365 /* Get the start of the nick to complete. */
1366 gtk_text_buffer_get_iter_at_mark (buffer, &start, gtk_text_buffer_get_insert (buffer));
1367 gtk_text_iter_backward_word_start (&start);
1368 is_start_of_buffer = gtk_text_iter_is_start (&start);
1370 list = empathy_contact_list_get_members (EMPATHY_CONTACT_LIST (priv->tp_chat));
1371 g_completion_add_items (priv->completion, list);
1373 nick = gtk_text_buffer_get_text (buffer, &start, &current, FALSE);
1374 completed_list = g_completion_complete (priv->completion,
1375 nick,
1376 &completed);
1378 g_free (nick);
1380 if (completed) {
1381 guint len;
1382 const gchar *text;
1383 gchar *complete_char = NULL;
1385 gtk_text_buffer_delete (buffer, &start, &current);
1387 len = g_list_length (completed_list);
1389 if (len == 1) {
1390 /* If we only have one hit, use that text
1391 * instead of the text in completed since the
1392 * completed text will use the typed string
1393 * which might be cased all wrong.
1394 * Fixes #120876
1395 * */
1396 text = empathy_contact_get_name (completed_list->data);
1397 } else {
1398 text = completed;
1401 gtk_text_buffer_insert_at_cursor (buffer, text, strlen (text));
1403 if (len == 1 && is_start_of_buffer &&
1404 empathy_conf_get_string (empathy_conf_get (),
1405 EMPATHY_PREFS_CHAT_NICK_COMPLETION_CHAR,
1406 &complete_char) &&
1407 complete_char != NULL) {
1408 gtk_text_buffer_insert_at_cursor (buffer,
1409 complete_char,
1410 strlen (complete_char));
1411 gtk_text_buffer_insert_at_cursor (buffer, " ", 1);
1412 g_free (complete_char);
1415 g_free (completed);
1418 g_completion_clear_items (priv->completion);
1420 g_list_foreach (list, (GFunc) g_object_unref, NULL);
1421 g_list_free (list);
1423 return TRUE;
1426 return FALSE;
1429 static gboolean
1430 chat_text_view_focus_in_event_cb (GtkWidget *widget,
1431 GdkEvent *event,
1432 EmpathyChat *chat)
1434 gtk_widget_grab_focus (chat->input_text_view);
1436 return TRUE;
1439 static gboolean
1440 chat_input_set_size_request_idle (gpointer sw)
1442 gtk_widget_set_size_request (sw, -1, MAX_INPUT_HEIGHT);
1444 return FALSE;
1447 static void
1448 chat_input_size_request_cb (GtkWidget *widget,
1449 GtkRequisition *requisition,
1450 EmpathyChat *chat)
1452 EmpathyChatPriv *priv = GET_PRIV (chat);
1453 GtkWidget *sw;
1455 sw = gtk_widget_get_parent (widget);
1456 if (requisition->height >= MAX_INPUT_HEIGHT && !priv->has_input_vscroll) {
1457 g_idle_add (chat_input_set_size_request_idle, sw);
1458 gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (sw),
1459 GTK_POLICY_NEVER,
1460 GTK_POLICY_ALWAYS);
1461 priv->has_input_vscroll = TRUE;
1464 if (requisition->height < MAX_INPUT_HEIGHT && priv->has_input_vscroll) {
1465 gtk_widget_set_size_request (sw, -1, -1);
1466 gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (sw),
1467 GTK_POLICY_NEVER,
1468 GTK_POLICY_NEVER);
1469 priv->has_input_vscroll = FALSE;
1473 static void
1474 chat_input_realize_cb (GtkWidget *widget,
1475 EmpathyChat *chat)
1477 DEBUG ("Setting focus to the input text view");
1478 if (gtk_widget_is_sensitive (widget)) {
1479 gtk_widget_grab_focus (widget);
1483 static void
1484 chat_insert_smiley_activate_cb (EmpathySmileyManager *manager,
1485 EmpathySmiley *smiley,
1486 gpointer user_data)
1488 EmpathyChat *chat = EMPATHY_CHAT (user_data);
1489 GtkTextBuffer *buffer;
1490 GtkTextIter iter;
1492 buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (chat->input_text_view));
1494 gtk_text_buffer_get_end_iter (buffer, &iter);
1495 gtk_text_buffer_insert (buffer, &iter, smiley->str, -1);
1497 gtk_text_buffer_get_end_iter (buffer, &iter);
1498 gtk_text_buffer_insert (buffer, &iter, " ", -1);
1501 typedef struct {
1502 EmpathyChat *chat;
1503 gchar *word;
1505 GtkTextIter start;
1506 GtkTextIter end;
1507 } EmpathyChatSpell;
1509 static EmpathyChatSpell *
1510 chat_spell_new (EmpathyChat *chat,
1511 const gchar *word,
1512 GtkTextIter start,
1513 GtkTextIter end)
1515 EmpathyChatSpell *chat_spell;
1517 chat_spell = g_slice_new0 (EmpathyChatSpell);
1519 chat_spell->chat = g_object_ref (chat);
1520 chat_spell->word = g_strdup (word);
1521 chat_spell->start = start;
1522 chat_spell->end = end;
1524 return chat_spell;
1527 static void
1528 chat_spell_free (EmpathyChatSpell *chat_spell)
1530 g_object_unref (chat_spell->chat);
1531 g_free (chat_spell->word);
1532 g_slice_free (EmpathyChatSpell, chat_spell);
1535 static void
1536 chat_spelling_menu_activate_cb (GtkMenuItem *menu_item,
1537 EmpathyChatSpell *chat_spell)
1539 empathy_chat_correct_word (chat_spell->chat,
1540 &(chat_spell->start),
1541 &(chat_spell->end),
1542 gtk_menu_item_get_label (menu_item));
1545 static GtkWidget *
1546 chat_spelling_build_menu (EmpathyChatSpell *chat_spell)
1548 GtkWidget *menu, *menu_item;
1549 GList *suggestions, *l;
1551 menu = gtk_menu_new ();
1552 suggestions = empathy_spell_get_suggestions (chat_spell->word);
1553 if (suggestions == NULL) {
1554 menu_item = gtk_menu_item_new_with_label (_("(No Suggestions)"));
1555 gtk_widget_set_sensitive (menu_item, FALSE);
1556 gtk_menu_shell_append (GTK_MENU_SHELL (menu), menu_item);
1557 } else {
1558 for (l = suggestions; l; l = l->next) {
1559 menu_item = gtk_menu_item_new_with_label (l->data);
1560 g_signal_connect (G_OBJECT (menu_item),
1561 "activate",
1562 G_CALLBACK (chat_spelling_menu_activate_cb),
1563 chat_spell);
1564 gtk_menu_shell_append (GTK_MENU_SHELL (menu), menu_item);
1567 empathy_spell_free_suggestions (suggestions);
1569 gtk_widget_show_all (menu);
1571 return menu;
1574 static void
1575 chat_text_send_cb (GtkMenuItem *menuitem,
1576 EmpathyChat *chat)
1578 chat_input_text_view_send (chat);
1581 static void
1582 chat_input_populate_popup_cb (GtkTextView *view,
1583 GtkMenu *menu,
1584 EmpathyChat *chat)
1586 EmpathyChatPriv *priv;
1587 GtkTextBuffer *buffer;
1588 GtkTextTagTable *table;
1589 GtkTextTag *tag;
1590 gint x, y;
1591 GtkTextIter iter, start, end;
1592 GtkWidget *item;
1593 gchar *str = NULL;
1594 EmpathyChatSpell *chat_spell;
1595 GtkWidget *spell_menu;
1596 EmpathySmileyManager *smiley_manager;
1597 GtkWidget *smiley_menu;
1598 GtkWidget *image;
1600 priv = GET_PRIV (chat);
1601 buffer = gtk_text_view_get_buffer (view);
1603 /* Add the emoticon menu. */
1604 item = gtk_separator_menu_item_new ();
1605 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1606 gtk_widget_show (item);
1608 item = gtk_image_menu_item_new_with_mnemonic (_("Insert Smiley"));
1609 image = gtk_image_new_from_icon_name ("face-smile",
1610 GTK_ICON_SIZE_MENU);
1611 gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), image);
1612 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1613 gtk_widget_show (item);
1615 smiley_manager = empathy_smiley_manager_dup_singleton ();
1616 smiley_menu = empathy_smiley_menu_new (smiley_manager,
1617 chat_insert_smiley_activate_cb,
1618 chat);
1619 gtk_menu_item_set_submenu (GTK_MENU_ITEM (item), smiley_menu);
1620 g_object_unref (smiley_manager);
1622 /* Add the Send menu item. */
1623 gtk_text_buffer_get_bounds (buffer, &start, &end);
1624 str = gtk_text_buffer_get_text (buffer, &start, &end, FALSE);
1625 if (!EMP_STR_EMPTY (str)) {
1626 item = gtk_menu_item_new_with_mnemonic (_("_Send"));
1627 g_signal_connect (G_OBJECT (item), "activate",
1628 G_CALLBACK (chat_text_send_cb), chat);
1629 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1630 gtk_widget_show (item);
1632 str = NULL;
1634 /* Add the spell check menu item. */
1635 table = gtk_text_buffer_get_tag_table (buffer);
1636 tag = gtk_text_tag_table_lookup (table, "misspelled");
1637 gtk_widget_get_pointer (GTK_WIDGET (view), &x, &y);
1638 gtk_text_view_window_to_buffer_coords (GTK_TEXT_VIEW (view),
1639 GTK_TEXT_WINDOW_WIDGET,
1640 x, y,
1641 &x, &y);
1642 gtk_text_view_get_iter_at_location (GTK_TEXT_VIEW (view), &iter, x, y);
1643 start = end = iter;
1644 if (gtk_text_iter_backward_to_tag_toggle (&start, tag) &&
1645 gtk_text_iter_forward_to_tag_toggle (&end, tag)) {
1647 str = gtk_text_buffer_get_text (buffer,
1648 &start, &end, FALSE);
1650 if (!EMP_STR_EMPTY (str)) {
1651 chat_spell = chat_spell_new (chat, str, start, end);
1652 g_object_set_data_full (G_OBJECT (menu),
1653 "chat_spell", chat_spell,
1654 (GDestroyNotify) chat_spell_free);
1656 item = gtk_separator_menu_item_new ();
1657 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1658 gtk_widget_show (item);
1660 item = gtk_image_menu_item_new_with_mnemonic (_("_Spelling Suggestions"));
1661 image = gtk_image_new_from_icon_name (GTK_STOCK_SPELL_CHECK,
1662 GTK_ICON_SIZE_MENU);
1663 gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (item), image);
1665 spell_menu = chat_spelling_build_menu (chat_spell);
1666 gtk_menu_item_set_submenu (GTK_MENU_ITEM (item), spell_menu);
1668 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item);
1669 gtk_widget_show (item);
1673 static gboolean
1674 chat_log_filter (EmpathyMessage *message,
1675 gpointer user_data)
1677 EmpathyChat *chat = (EmpathyChat *) user_data;
1678 EmpathyChatPriv *priv = GET_PRIV (chat);
1679 const GList *pending;
1681 pending = empathy_tp_chat_get_pending_messages (priv->tp_chat);
1683 for (; pending; pending = g_list_next (pending)) {
1684 if (empathy_message_equal (message, pending->data)) {
1685 return FALSE;
1689 return TRUE;
1692 static void
1693 chat_add_logs (EmpathyChat *chat)
1695 EmpathyChatPriv *priv = GET_PRIV (chat);
1696 gboolean is_chatroom;
1697 GList *messages, *l;
1699 if (!priv->id) {
1700 return;
1703 /* Turn off scrolling temporarily */
1704 empathy_chat_view_scroll (chat->view, FALSE);
1706 /* Add messages from last conversation */
1707 is_chatroom = priv->handle_type == TP_HANDLE_TYPE_ROOM;
1709 messages = empathy_log_manager_get_filtered_messages (priv->log_manager,
1710 priv->account,
1711 priv->id,
1712 is_chatroom,
1714 chat_log_filter,
1715 chat);
1717 for (l = messages; l; l = g_list_next (l)) {
1718 empathy_chat_view_append_message (chat->view, l->data);
1719 g_object_unref (l->data);
1722 g_list_free (messages);
1724 /* Turn back on scrolling */
1725 empathy_chat_view_scroll (chat->view, TRUE);
1728 static gint
1729 chat_contacts_completion_func (const gchar *s1,
1730 const gchar *s2,
1731 gsize n)
1733 gchar *tmp, *nick1, *nick2;
1734 gint ret;
1736 if (s1 == s2) {
1737 return 0;
1739 if (!s1 || !s2) {
1740 return s1 ? -1 : +1;
1743 tmp = g_utf8_normalize (s1, -1, G_NORMALIZE_DEFAULT);
1744 nick1 = g_utf8_casefold (tmp, -1);
1745 g_free (tmp);
1747 tmp = g_utf8_normalize (s2, -1, G_NORMALIZE_DEFAULT);
1748 nick2 = g_utf8_casefold (tmp, -1);
1749 g_free (tmp);
1751 ret = strncmp (nick1, nick2, n);
1753 g_free (nick1);
1754 g_free (nick2);
1756 return ret;
1759 static gchar *
1760 build_part_message (guint reason,
1761 const gchar *name,
1762 EmpathyContact *actor,
1763 const gchar *message)
1765 GString *s = g_string_new ("");
1766 const gchar *actor_name = NULL;
1768 if (actor != NULL) {
1769 actor_name = empathy_contact_get_name (actor);
1772 /* Having an actor only really makes sense for a few actions... */
1773 switch (reason) {
1774 case TP_CHANNEL_GROUP_CHANGE_REASON_OFFLINE:
1775 g_string_append_printf (s, _("%s has disconnected"), name);
1776 break;
1777 case TP_CHANNEL_GROUP_CHANGE_REASON_KICKED:
1778 if (actor_name != NULL) {
1779 /* translators: reverse the order of these arguments
1780 * if the kicked should come before the kicker in your locale.
1782 g_string_append_printf (s, _("%1$s was kicked by %2$s"),
1783 name, actor_name);
1784 } else {
1785 g_string_append_printf (s, _("%s was kicked"), name);
1787 break;
1788 case TP_CHANNEL_GROUP_CHANGE_REASON_BANNED:
1789 if (actor_name != NULL) {
1790 /* translators: reverse the order of these arguments
1791 * if the banned should come before the banner in your locale.
1793 g_string_append_printf (s, _("%1$s was banned by %2$s"),
1794 name, actor_name);
1795 } else {
1796 g_string_append_printf (s, _("%s was banned"), name);
1798 break;
1799 default:
1800 g_string_append_printf (s, _("%s has left the room"), name);
1803 if (!EMP_STR_EMPTY (message)) {
1804 /* Note to translators: this string is appended to
1805 * notifications like "foo has left the room", with the message
1806 * given by the user living the room. If this poses a problem,
1807 * please let us know. :-)
1809 g_string_append_printf (s, _(" (%s)"), message);
1812 return g_string_free (s, FALSE);
1815 static void
1816 chat_members_changed_cb (EmpathyTpChat *tp_chat,
1817 EmpathyContact *contact,
1818 EmpathyContact *actor,
1819 guint reason,
1820 gchar *message,
1821 gboolean is_member,
1822 EmpathyChat *chat)
1824 EmpathyChatPriv *priv = GET_PRIV (chat);
1825 const gchar *name = empathy_contact_get_name (contact);
1826 gchar *str;
1828 g_return_if_fail (TP_CHANNEL_GROUP_CHANGE_REASON_RENAMED != reason);
1830 if (priv->block_events_timeout_id != 0)
1831 return;
1833 if (is_member) {
1834 str = g_strdup_printf (_("%s has joined the room"),
1835 name);
1836 } else {
1837 str = build_part_message (reason, name, actor, message);
1840 empathy_chat_view_append_event (chat->view, str);
1841 g_free (str);
1844 static void
1845 chat_member_renamed_cb (EmpathyTpChat *tp_chat,
1846 EmpathyContact *old_contact,
1847 EmpathyContact *new_contact,
1848 guint reason,
1849 gchar *message,
1850 EmpathyChat *chat)
1852 EmpathyChatPriv *priv = GET_PRIV (chat);
1854 g_return_if_fail (TP_CHANNEL_GROUP_CHANGE_REASON_RENAMED == reason);
1856 if (priv->block_events_timeout_id == 0) {
1857 gchar *str;
1859 str = g_strdup_printf (_("%s is now known as %s"),
1860 empathy_contact_get_name (old_contact),
1861 empathy_contact_get_name (new_contact));
1862 empathy_chat_view_append_event (chat->view, str);
1863 g_free (str);
1868 static gboolean
1869 chat_reset_size_request (gpointer widget)
1871 gtk_widget_set_size_request (widget, -1, -1);
1873 return FALSE;
1876 static void
1877 chat_update_contacts_visibility (EmpathyChat *chat,
1878 gboolean show)
1880 EmpathyChatPriv *priv = GET_PRIV (chat);
1881 GtkAllocation allocation;
1883 if (!priv->scrolled_window_contacts) {
1884 return;
1887 if (priv->remote_contact != NULL) {
1888 show = FALSE;
1891 if (show && priv->contact_list_view == NULL) {
1892 EmpathyContactListStore *store;
1893 gint min_width;
1895 /* We are adding the contact list to the chat, we don't want the
1896 * chat view to become too small. If the chat view is already
1897 * smaller than 250 make sure that size won't change. If the
1898 * chat view is bigger the contact list will take some space on
1899 * it but we make sure the chat view don't become smaller than
1900 * 250. Relax the size request once the resize is done */
1901 gtk_widget_get_allocation (priv->vbox_left, &allocation);
1902 min_width = MIN (allocation.width, 250);
1903 gtk_widget_set_size_request (priv->vbox_left, min_width, -1);
1904 g_idle_add (chat_reset_size_request, priv->vbox_left);
1906 if (priv->contacts_width > 0) {
1907 gtk_paned_set_position (GTK_PANED (priv->hpaned),
1908 priv->contacts_width);
1911 store = empathy_contact_list_store_new (EMPATHY_CONTACT_LIST (priv->tp_chat));
1912 priv->contact_list_view = GTK_WIDGET (empathy_contact_list_view_new (store,
1913 EMPATHY_CONTACT_LIST_FEATURE_CONTACT_TOOLTIP,
1914 EMPATHY_CONTACT_FEATURE_CHAT |
1915 EMPATHY_CONTACT_FEATURE_CALL |
1916 EMPATHY_CONTACT_FEATURE_LOG |
1917 EMPATHY_CONTACT_FEATURE_INFO));
1918 gtk_container_add (GTK_CONTAINER (priv->scrolled_window_contacts),
1919 priv->contact_list_view);
1920 gtk_widget_show (priv->contact_list_view);
1921 gtk_widget_show (priv->scrolled_window_contacts);
1922 g_object_unref (store);
1923 } else if (!show) {
1924 priv->contacts_width = gtk_paned_get_position (GTK_PANED (priv->hpaned));
1925 gtk_widget_hide (priv->scrolled_window_contacts);
1926 if (priv->contact_list_view != NULL) {
1927 gtk_widget_destroy (priv->contact_list_view);
1928 priv->contact_list_view = NULL;
1933 void
1934 empathy_chat_set_show_contacts (EmpathyChat *chat,
1935 gboolean show)
1937 EmpathyChatPriv *priv = GET_PRIV (chat);
1939 priv->show_contacts = show;
1941 chat_update_contacts_visibility (chat, show);
1943 g_object_notify (G_OBJECT (chat), "show-contacts");
1946 static void
1947 chat_remote_contact_changed_cb (EmpathyChat *chat)
1949 EmpathyChatPriv *priv = GET_PRIV (chat);
1951 if (priv->remote_contact != NULL) {
1952 g_object_unref (priv->remote_contact);
1953 priv->remote_contact = NULL;
1956 g_free (priv->id);
1958 priv->id = g_strdup (empathy_tp_chat_get_id (priv->tp_chat));
1959 priv->remote_contact = empathy_tp_chat_get_remote_contact (priv->tp_chat);
1960 if (priv->remote_contact != NULL) {
1961 g_object_ref (priv->remote_contact);
1962 priv->handle_type = TP_HANDLE_TYPE_CONTACT;
1964 else if (priv->tp_chat != NULL) {
1965 TpChannel *channel;
1967 channel = empathy_tp_chat_get_channel (priv->tp_chat);
1968 g_object_get (channel, "handle-type", &priv->handle_type, NULL);
1971 chat_update_contacts_visibility (chat, priv->show_contacts);
1973 g_object_notify (G_OBJECT (chat), "remote-contact");
1974 g_object_notify (G_OBJECT (chat), "id");
1977 static void
1978 chat_destroy_cb (EmpathyTpChat *tp_chat,
1979 EmpathyChat *chat)
1981 EmpathyChatPriv *priv;
1983 priv = GET_PRIV (chat);
1985 if (!priv->tp_chat) {
1986 return;
1989 chat_composing_remove_timeout (chat);
1990 g_object_unref (priv->tp_chat);
1991 priv->tp_chat = NULL;
1992 g_object_notify (G_OBJECT (chat), "tp-chat");
1994 empathy_chat_view_append_event (chat->view, _("Disconnected"));
1995 gtk_widget_set_sensitive (chat->input_text_view, FALSE);
1997 chat_update_contacts_visibility (chat, FALSE);
2000 static gboolean
2001 chat_hpaned_pos_changed_cb (GtkWidget* hpaned, gpointer user_data)
2003 gint hpaned_pos;
2004 hpaned_pos = gtk_paned_get_position (GTK_PANED(hpaned));
2005 empathy_conf_set_int (empathy_conf_get (),
2006 EMPATHY_PREFS_UI_CHAT_WINDOW_PANED_POS,
2007 hpaned_pos);
2008 return TRUE;
2012 static void
2013 show_pending_messages (EmpathyChat *chat) {
2014 EmpathyChatPriv *priv = GET_PRIV (chat);
2015 const GList *messages, *l;
2017 if (chat->view == NULL || priv->tp_chat == NULL)
2018 return;
2020 if (!priv->can_show_pending)
2021 return;
2023 messages = empathy_tp_chat_get_pending_messages (priv->tp_chat);
2025 for (l = messages; l != NULL ; l = g_list_next (l)) {
2026 EmpathyMessage *message = EMPATHY_MESSAGE (l->data);
2027 chat_message_received (chat, message);
2029 empathy_tp_chat_acknowledge_messages (priv->tp_chat, messages);
2032 static void
2033 chat_create_ui (EmpathyChat *chat)
2035 EmpathyChatPriv *priv = GET_PRIV (chat);
2036 GtkBuilder *gui;
2037 GList *list = NULL;
2038 gchar *filename;
2039 GtkTextBuffer *buffer;
2040 gint paned_pos;
2042 filename = empathy_file_lookup ("empathy-chat.ui",
2043 "libempathy-gtk");
2044 gui = empathy_builder_get_file (filename,
2045 "chat_widget", &priv->widget,
2046 "hpaned", &priv->hpaned,
2047 "vbox_left", &priv->vbox_left,
2048 "scrolled_window_chat", &priv->scrolled_window_chat,
2049 "scrolled_window_input", &priv->scrolled_window_input,
2050 "hbox_topic", &priv->hbox_topic,
2051 "label_topic", &priv->label_topic,
2052 "scrolled_window_contacts", &priv->scrolled_window_contacts,
2053 "info_bar_vbox", &priv->info_bar_vbox,
2054 NULL);
2055 g_free (filename);
2057 /* Add message view. */
2058 chat->view = empathy_theme_manager_create_view (empathy_theme_manager_get ());
2059 /* If this is a GtkTextView, it's set as a drag destination for text/plain
2060 and other types, even though it's non-editable and doesn't accept any
2061 drags. This steals drag motion for anything inside the scrollbars,
2062 making drag destinations on chat windows far less useful.
2064 gtk_drag_dest_unset (GTK_WIDGET (chat->view));
2065 g_signal_connect (chat->view, "focus_in_event",
2066 G_CALLBACK (chat_text_view_focus_in_event_cb),
2067 chat);
2068 gtk_container_add (GTK_CONTAINER (priv->scrolled_window_chat),
2069 GTK_WIDGET (chat->view));
2070 gtk_widget_show (GTK_WIDGET (chat->view));
2072 /* Add input GtkTextView */
2073 chat->input_text_view = g_object_new (GTK_TYPE_TEXT_VIEW,
2074 "pixels-above-lines", 2,
2075 "pixels-below-lines", 2,
2076 "pixels-inside-wrap", 1,
2077 "right-margin", 2,
2078 "left-margin", 2,
2079 "wrap-mode", GTK_WRAP_WORD_CHAR,
2080 NULL);
2081 g_signal_connect (chat->input_text_view, "key-press-event",
2082 G_CALLBACK (chat_input_key_press_event_cb),
2083 chat);
2084 g_signal_connect (chat->input_text_view, "size-request",
2085 G_CALLBACK (chat_input_size_request_cb),
2086 chat);
2087 g_signal_connect (chat->input_text_view, "realize",
2088 G_CALLBACK (chat_input_realize_cb),
2089 chat);
2090 g_signal_connect (chat->input_text_view, "populate-popup",
2091 G_CALLBACK (chat_input_populate_popup_cb),
2092 chat);
2093 buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (chat->input_text_view));
2094 g_signal_connect (buffer, "changed",
2095 G_CALLBACK (chat_input_text_buffer_changed_cb),
2096 chat);
2097 gtk_text_buffer_create_tag (buffer, "misspelled",
2098 "underline", PANGO_UNDERLINE_ERROR,
2099 NULL);
2100 gtk_container_add (GTK_CONTAINER (priv->scrolled_window_input),
2101 chat->input_text_view);
2102 gtk_widget_show (chat->input_text_view);
2104 /* Initialy hide the topic, will be shown if not empty */
2105 gtk_widget_hide (priv->hbox_topic);
2107 g_signal_connect (priv->hpaned, "notify::position",
2108 G_CALLBACK (chat_hpaned_pos_changed_cb),
2109 NULL);
2111 /* Load the paned position */
2112 if (empathy_conf_get_int (empathy_conf_get (),
2113 EMPATHY_PREFS_UI_CHAT_WINDOW_PANED_POS,
2114 &paned_pos)
2115 && paned_pos)
2116 gtk_paned_set_position (GTK_PANED(priv->hpaned), paned_pos);
2118 /* Set widget focus order */
2119 list = g_list_append (NULL, priv->scrolled_window_input);
2120 gtk_container_set_focus_chain (GTK_CONTAINER (priv->vbox_left), list);
2121 g_list_free (list);
2123 list = g_list_append (NULL, priv->vbox_left);
2124 list = g_list_append (list, priv->scrolled_window_contacts);
2125 gtk_container_set_focus_chain (GTK_CONTAINER (priv->hpaned), list);
2126 g_list_free (list);
2128 list = g_list_append (NULL, priv->hpaned);
2129 list = g_list_append (list, priv->hbox_topic);
2130 gtk_container_set_focus_chain (GTK_CONTAINER (priv->widget), list);
2131 g_list_free (list);
2133 /* Add the main widget in the chat widget */
2134 gtk_container_add (GTK_CONTAINER (chat), priv->widget);
2135 g_object_unref (gui);
2138 static void
2139 chat_size_request (GtkWidget *widget,
2140 GtkRequisition *requisition)
2142 GtkBin *bin = GTK_BIN (widget);
2143 GtkWidget *child;
2145 requisition->width = gtk_container_get_border_width (GTK_CONTAINER (widget)) * 2;
2146 requisition->height = gtk_container_get_border_width (GTK_CONTAINER (widget)) * 2;
2148 child = gtk_bin_get_child (bin);
2150 if (child && gtk_widget_get_visible (child))
2152 GtkRequisition child_requisition;
2154 gtk_widget_size_request (child, &child_requisition);
2156 requisition->width += child_requisition.width;
2157 requisition->height += child_requisition.height;
2161 static void
2162 chat_size_allocate (GtkWidget *widget,
2163 GtkAllocation *allocation)
2165 GtkBin *bin = GTK_BIN (widget);
2166 GtkAllocation child_allocation;
2167 GtkWidget *child;
2169 gtk_widget_set_allocation (widget, allocation);
2171 child = gtk_bin_get_child (bin);
2173 if (child && gtk_widget_get_visible (child))
2175 child_allocation.x = allocation->x + gtk_container_get_border_width (GTK_CONTAINER (widget));
2176 child_allocation.y = allocation->y + gtk_container_get_border_width (GTK_CONTAINER (widget));
2177 child_allocation.width = MAX (allocation->width - gtk_container_get_border_width (GTK_CONTAINER (widget)) * 2, 0);
2178 child_allocation.height = MAX (allocation->height - gtk_container_get_border_width (GTK_CONTAINER (widget)) * 2, 0);
2180 gtk_widget_size_allocate (child, &child_allocation);
2184 static void
2185 chat_finalize (GObject *object)
2187 EmpathyChat *chat;
2188 EmpathyChatPriv *priv;
2190 chat = EMPATHY_CHAT (object);
2191 priv = GET_PRIV (chat);
2193 DEBUG ("Finalized: %p", object);
2195 g_list_foreach (priv->input_history, (GFunc) chat_input_history_entry_free, NULL);
2196 g_list_free (priv->input_history);
2198 g_list_foreach (priv->compositors, (GFunc) g_object_unref, NULL);
2199 g_list_free (priv->compositors);
2201 chat_composing_remove_timeout (chat);
2203 g_object_unref (priv->account_manager);
2204 g_object_unref (priv->log_manager);
2206 if (priv->tp_chat) {
2207 g_signal_handlers_disconnect_by_func (priv->tp_chat,
2208 chat_destroy_cb, chat);
2209 g_signal_handlers_disconnect_by_func (priv->tp_chat,
2210 chat_message_received_cb, chat);
2211 g_signal_handlers_disconnect_by_func (priv->tp_chat,
2212 chat_send_error_cb, chat);
2213 g_signal_handlers_disconnect_by_func (priv->tp_chat,
2214 chat_state_changed_cb, chat);
2215 g_signal_handlers_disconnect_by_func (priv->tp_chat,
2216 chat_property_changed_cb, chat);
2217 g_signal_handlers_disconnect_by_func (priv->tp_chat,
2218 chat_members_changed_cb, chat);
2219 g_signal_handlers_disconnect_by_func (priv->tp_chat,
2220 chat_remote_contact_changed_cb, chat);
2221 empathy_tp_chat_close (priv->tp_chat);
2222 g_object_unref (priv->tp_chat);
2224 if (priv->account) {
2225 g_object_unref (priv->account);
2227 if (priv->remote_contact) {
2228 g_object_unref (priv->remote_contact);
2231 if (priv->block_events_timeout_id) {
2232 g_source_remove (priv->block_events_timeout_id);
2235 g_free (priv->id);
2236 g_free (priv->name);
2237 g_free (priv->subject);
2238 g_completion_free (priv->completion);
2240 G_OBJECT_CLASS (empathy_chat_parent_class)->finalize (object);
2243 static void
2244 chat_constructed (GObject *object)
2246 EmpathyChat *chat = EMPATHY_CHAT (object);
2247 EmpathyChatPriv *priv = GET_PRIV (chat);
2249 if (priv->handle_type != TP_HANDLE_TYPE_ROOM)
2250 chat_add_logs (chat);
2251 priv->can_show_pending = TRUE;
2252 show_pending_messages (chat);
2255 static void
2256 empathy_chat_class_init (EmpathyChatClass *klass)
2258 GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
2259 GObjectClass *object_class = G_OBJECT_CLASS (klass);
2261 object_class->finalize = chat_finalize;
2262 object_class->get_property = chat_get_property;
2263 object_class->set_property = chat_set_property;
2264 object_class->constructed = chat_constructed;
2266 widget_class->size_request = chat_size_request;
2267 widget_class->size_allocate = chat_size_allocate;
2269 g_object_class_install_property (object_class,
2270 PROP_TP_CHAT,
2271 g_param_spec_object ("tp-chat",
2272 "Empathy tp chat",
2273 "The tp chat object",
2274 EMPATHY_TYPE_TP_CHAT,
2275 G_PARAM_CONSTRUCT |
2276 G_PARAM_READWRITE |
2277 G_PARAM_STATIC_STRINGS));
2278 g_object_class_install_property (object_class,
2279 PROP_ACCOUNT,
2280 g_param_spec_object ("account",
2281 "Account of the chat",
2282 "The account of the chat",
2283 TP_TYPE_ACCOUNT,
2284 G_PARAM_READABLE |
2285 G_PARAM_STATIC_STRINGS));
2286 g_object_class_install_property (object_class,
2287 PROP_ID,
2288 g_param_spec_string ("id",
2289 "Chat's id",
2290 "The id of the chat",
2291 NULL,
2292 G_PARAM_READABLE |
2293 G_PARAM_STATIC_STRINGS));
2294 g_object_class_install_property (object_class,
2295 PROP_NAME,
2296 g_param_spec_string ("name",
2297 "Chat's name",
2298 "The name of the chat",
2299 NULL,
2300 G_PARAM_READABLE |
2301 G_PARAM_STATIC_STRINGS));
2302 g_object_class_install_property (object_class,
2303 PROP_SUBJECT,
2304 g_param_spec_string ("subject",
2305 "Chat's subject",
2306 "The subject or topic of the chat",
2307 NULL,
2308 G_PARAM_READABLE |
2309 G_PARAM_STATIC_STRINGS));
2310 g_object_class_install_property (object_class,
2311 PROP_REMOTE_CONTACT,
2312 g_param_spec_object ("remote-contact",
2313 "The remote contact",
2314 "The remote contact is any",
2315 EMPATHY_TYPE_CONTACT,
2316 G_PARAM_READABLE |
2317 G_PARAM_STATIC_STRINGS));
2318 g_object_class_install_property (object_class,
2319 PROP_SHOW_CONTACTS,
2320 g_param_spec_boolean ("show-contacts",
2321 "Contacts' visibility",
2322 "The visibility of the contacts' list",
2323 TRUE,
2324 G_PARAM_READWRITE |
2325 G_PARAM_STATIC_STRINGS));
2327 signals[COMPOSING] =
2328 g_signal_new ("composing",
2329 G_OBJECT_CLASS_TYPE (object_class),
2330 G_SIGNAL_RUN_LAST,
2332 NULL, NULL,
2333 g_cclosure_marshal_VOID__BOOLEAN,
2334 G_TYPE_NONE,
2335 1, G_TYPE_BOOLEAN);
2337 signals[NEW_MESSAGE] =
2338 g_signal_new ("new-message",
2339 G_OBJECT_CLASS_TYPE (object_class),
2340 G_SIGNAL_RUN_LAST,
2342 NULL, NULL,
2343 g_cclosure_marshal_VOID__OBJECT,
2344 G_TYPE_NONE,
2345 1, EMPATHY_TYPE_MESSAGE);
2347 g_type_class_add_private (object_class, sizeof (EmpathyChatPriv));
2350 static gboolean
2351 chat_block_events_timeout_cb (gpointer data)
2353 EmpathyChatPriv *priv = GET_PRIV (data);
2355 priv->block_events_timeout_id = 0;
2357 return FALSE;
2360 static void
2361 account_manager_prepared_cb (GObject *source_object,
2362 GAsyncResult *result,
2363 gpointer user_data)
2365 GList *accounts, *l;
2366 TpAccountManager *account_manager = TP_ACCOUNT_MANAGER (source_object);
2367 EmpathyChat *chat = user_data;
2368 GError *error = NULL;
2370 if (!tp_account_manager_prepare_finish (account_manager, result, &error)) {
2371 DEBUG ("Failed to prepare the account manager: %s", error->message);
2372 g_error_free (error);
2373 return;
2376 accounts = tp_account_manager_get_valid_accounts (account_manager);
2378 for (l = accounts; l != NULL; l = l->next) {
2379 TpAccount *account = l->data;
2380 empathy_signal_connect_weak (account, "status-changed",
2381 G_CALLBACK (chat_new_connection_cb),
2382 G_OBJECT (chat));
2385 g_list_free (accounts);
2388 static void
2389 empathy_chat_init (EmpathyChat *chat)
2391 EmpathyChatPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (chat,
2392 EMPATHY_TYPE_CHAT, EmpathyChatPriv);
2394 chat->priv = priv;
2395 priv->log_manager = empathy_log_manager_dup_singleton ();
2396 priv->contacts_width = -1;
2397 priv->input_history = NULL;
2398 priv->input_history_current = NULL;
2399 priv->account_manager = tp_account_manager_dup ();
2401 tp_account_manager_prepare_async (priv->account_manager, NULL,
2402 account_manager_prepared_cb, chat);
2404 empathy_conf_get_bool (empathy_conf_get (),
2405 EMPATHY_PREFS_CHAT_SHOW_CONTACTS_IN_ROOMS,
2406 &priv->show_contacts);
2408 /* Block events for some time to avoid having "has come online" or
2409 * "joined" messages. */
2410 priv->block_events_timeout_id =
2411 g_timeout_add_seconds (1, chat_block_events_timeout_cb, chat);
2413 /* Add nick name completion */
2414 priv->completion = g_completion_new ((GCompletionFunc) empathy_contact_get_name);
2415 g_completion_set_compare (priv->completion, chat_contacts_completion_func);
2417 chat_create_ui (chat);
2420 EmpathyChat *
2421 empathy_chat_new (EmpathyTpChat *tp_chat)
2423 return g_object_new (EMPATHY_TYPE_CHAT, "tp-chat", tp_chat, NULL);
2426 EmpathyTpChat *
2427 empathy_chat_get_tp_chat (EmpathyChat *chat)
2429 EmpathyChatPriv *priv = GET_PRIV (chat);
2431 g_return_val_if_fail (EMPATHY_IS_CHAT (chat), NULL);
2433 return priv->tp_chat;
2436 static void display_password_info_bar (EmpathyChat *self,
2437 gboolean retry);
2439 static void
2440 provide_password_cb (GObject *tp_chat,
2441 GAsyncResult *res,
2442 gpointer user_data)
2444 EmpathyChat *self = EMPATHY_CHAT (user_data);
2445 EmpathyChatPriv *priv = GET_PRIV (self);
2446 GError *error = NULL;
2448 if (!empathy_tp_chat_provide_password_finish (EMPATHY_TP_CHAT (tp_chat), res,
2449 &error)) {
2450 DEBUG ("error: %s", error->message);
2451 /* FIXME: what should we do if that's another error? Close the channel?
2452 * Display the raw D-Bus error to the user isn't very useful */
2453 if (g_error_matches (error, TP_ERRORS, TP_ERROR_AUTHENTICATION_FAILED))
2454 display_password_info_bar (self, TRUE);
2455 g_error_free (error);
2456 return;
2459 /* Room joined */
2460 gtk_widget_set_sensitive (priv->hpaned, TRUE);
2461 gtk_widget_grab_focus (self->input_text_view);
2464 static void
2465 password_infobar_response_cb (GtkWidget *info_bar,
2466 gint response_id,
2467 EmpathyChat *self)
2469 EmpathyChatPriv *priv = GET_PRIV (self);
2470 GtkWidget *entry;
2471 const gchar *password;
2473 if (response_id != GTK_RESPONSE_OK)
2474 goto out;
2476 entry = g_object_get_data (G_OBJECT (info_bar), "password-entry");
2477 g_assert (entry != NULL);
2479 password = gtk_entry_get_text (GTK_ENTRY (entry));
2481 empathy_tp_chat_provide_password_async (priv->tp_chat, password,
2482 provide_password_cb, self);
2484 out:
2485 gtk_widget_destroy (info_bar);
2488 static void
2489 password_entry_activate_cb (GtkWidget *entry,
2490 GtkWidget *info_bar)
2492 gtk_info_bar_response (GTK_INFO_BAR (info_bar), GTK_RESPONSE_OK);
2495 static void
2496 passwd_join_button_cb (GtkButton *button,
2497 GtkWidget *info_bar)
2499 gtk_info_bar_response (GTK_INFO_BAR (info_bar), GTK_RESPONSE_OK);
2502 static void
2503 display_password_info_bar (EmpathyChat *self,
2504 gboolean retry)
2506 EmpathyChatPriv *priv = GET_PRIV (self);
2507 GtkWidget *info_bar;
2508 GtkWidget *content_area;
2509 GtkWidget *hbox;
2510 GtkWidget *image;
2511 GtkWidget *label;
2512 GtkWidget *entry;
2513 GtkWidget *alig;
2514 GtkWidget *button;
2515 GtkMessageType type;
2516 const gchar *msg, *button_label;
2518 if (retry) {
2519 /* Previous password was wrong */
2520 type = GTK_MESSAGE_ERROR;
2521 msg = _("Wrong password; please try again:");
2522 button_label = _("Retry");
2524 else {
2525 /* First time we're trying to join */
2526 type = GTK_MESSAGE_QUESTION;
2527 msg = _("This room is protected by a password:");
2528 button_label = _("Join");
2531 info_bar = gtk_info_bar_new ();
2532 gtk_info_bar_set_message_type (GTK_INFO_BAR (info_bar), type);
2534 content_area = gtk_info_bar_get_content_area (GTK_INFO_BAR (info_bar));
2536 hbox = gtk_hbox_new (FALSE, 3);
2537 gtk_container_add (GTK_CONTAINER (content_area), hbox);
2539 /* Add image */
2540 image = gtk_image_new_from_stock (GTK_STOCK_DIALOG_AUTHENTICATION,
2541 GTK_ICON_SIZE_DIALOG);
2542 gtk_box_pack_start (GTK_BOX (hbox), image, FALSE, FALSE, 0);
2544 /* Add message */
2545 label = gtk_label_new (msg);
2546 gtk_box_pack_start (GTK_BOX (hbox), label, FALSE, FALSE, 0);
2548 /* Add password entry */
2549 entry = gtk_entry_new ();
2550 gtk_entry_set_visibility (GTK_ENTRY (entry), FALSE);
2551 gtk_box_pack_start (GTK_BOX (hbox), entry, TRUE, TRUE, 0);
2553 g_signal_connect (entry, "activate",
2554 G_CALLBACK (password_entry_activate_cb), info_bar);
2556 /* Focus the password entry once it's realized */
2557 g_signal_connect (entry, "realize", G_CALLBACK (gtk_widget_grab_focus), NULL);
2559 /* Add 'Join' button */
2560 alig = gtk_alignment_new (0, 0.5, 0, 0);
2562 button = gtk_button_new_with_label (button_label);
2563 gtk_container_add (GTK_CONTAINER (alig), button);
2564 gtk_box_pack_start (GTK_BOX (hbox), alig, FALSE, FALSE, 0);
2566 g_signal_connect (button, "clicked", G_CALLBACK (passwd_join_button_cb),
2567 info_bar);
2569 g_object_set_data (G_OBJECT (info_bar), "password-entry", entry);
2571 gtk_box_pack_start (GTK_BOX (priv->info_bar_vbox), info_bar,
2572 FALSE, FALSE, 3);
2573 gtk_widget_show_all (hbox);
2575 g_signal_connect (info_bar, "response",
2576 G_CALLBACK (password_infobar_response_cb), self);
2578 gtk_widget_show_all (info_bar);
2581 static void
2582 chat_password_needed_changed_cb (EmpathyChat *self)
2584 EmpathyChatPriv *priv = GET_PRIV (self);
2586 if (empathy_tp_chat_password_needed (priv->tp_chat)) {
2587 display_password_info_bar (self, FALSE);
2588 gtk_widget_set_sensitive (priv->hpaned, FALSE);
2592 void
2593 empathy_chat_set_tp_chat (EmpathyChat *chat,
2594 EmpathyTpChat *tp_chat)
2596 EmpathyChatPriv *priv = GET_PRIV (chat);
2597 TpConnection *connection;
2598 GPtrArray *properties;
2600 g_return_if_fail (EMPATHY_IS_CHAT (chat));
2601 g_return_if_fail (EMPATHY_IS_TP_CHAT (tp_chat));
2602 g_return_if_fail (empathy_tp_chat_is_ready (tp_chat));
2604 if (priv->tp_chat) {
2605 return;
2608 if (priv->account) {
2609 g_object_unref (priv->account);
2612 priv->tp_chat = g_object_ref (tp_chat);
2613 connection = empathy_tp_chat_get_connection (priv->tp_chat);
2614 priv->account = g_object_ref (empathy_get_account_for_connection (connection));
2616 g_signal_connect (tp_chat, "destroy",
2617 G_CALLBACK (chat_destroy_cb),
2618 chat);
2619 g_signal_connect (tp_chat, "message-received",
2620 G_CALLBACK (chat_message_received_cb),
2621 chat);
2622 g_signal_connect (tp_chat, "send-error",
2623 G_CALLBACK (chat_send_error_cb),
2624 chat);
2625 g_signal_connect (tp_chat, "chat-state-changed",
2626 G_CALLBACK (chat_state_changed_cb),
2627 chat);
2628 g_signal_connect (tp_chat, "property-changed",
2629 G_CALLBACK (chat_property_changed_cb),
2630 chat);
2631 g_signal_connect (tp_chat, "members-changed",
2632 G_CALLBACK (chat_members_changed_cb),
2633 chat);
2634 g_signal_connect (tp_chat, "member-renamed",
2635 G_CALLBACK (chat_member_renamed_cb),
2636 chat);
2637 g_signal_connect_swapped (tp_chat, "notify::remote-contact",
2638 G_CALLBACK (chat_remote_contact_changed_cb),
2639 chat);
2640 g_signal_connect_swapped (tp_chat, "notify::password-needed",
2641 G_CALLBACK (chat_password_needed_changed_cb),
2642 chat);
2644 /* Get initial value of properties */
2645 properties = empathy_tp_chat_get_properties (priv->tp_chat);
2646 if (properties != NULL) {
2647 guint i;
2649 for (i = 0; i < properties->len; i++) {
2650 EmpathyTpChatProperty *property;
2652 property = g_ptr_array_index (properties, i);
2653 if (property->value == NULL)
2654 continue;
2656 chat_property_changed_cb (priv->tp_chat,
2657 property->name,
2658 property->value,
2659 chat);
2663 chat_remote_contact_changed_cb (chat);
2665 if (chat->input_text_view) {
2666 gtk_widget_set_sensitive (chat->input_text_view, TRUE);
2667 if (priv->block_events_timeout_id == 0) {
2668 empathy_chat_view_append_event (chat->view, _("Connected"));
2672 g_object_notify (G_OBJECT (chat), "tp-chat");
2673 g_object_notify (G_OBJECT (chat), "id");
2674 g_object_notify (G_OBJECT (chat), "account");
2676 /* This is a noop when tp-chat is set at object construction time and causes
2677 * the pending messages to be show when it's set on the object after it has
2678 * been created */
2679 show_pending_messages (chat);
2681 /* check if a password is needed */
2682 chat_password_needed_changed_cb (chat);
2685 TpAccount *
2686 empathy_chat_get_account (EmpathyChat *chat)
2688 EmpathyChatPriv *priv = GET_PRIV (chat);
2690 g_return_val_if_fail (EMPATHY_IS_CHAT (chat), NULL);
2692 return priv->account;
2695 const gchar *
2696 empathy_chat_get_id (EmpathyChat *chat)
2698 EmpathyChatPriv *priv = GET_PRIV (chat);
2700 g_return_val_if_fail (EMPATHY_IS_CHAT (chat), NULL);
2702 return priv->id;
2705 const gchar *
2706 empathy_chat_get_name (EmpathyChat *chat)
2708 EmpathyChatPriv *priv = GET_PRIV (chat);
2709 const gchar *ret;
2711 g_return_val_if_fail (EMPATHY_IS_CHAT (chat), NULL);
2713 ret = priv->name;
2714 if (!ret && priv->remote_contact) {
2715 ret = empathy_contact_get_name (priv->remote_contact);
2718 if (!ret)
2719 ret = priv->id;
2721 return ret ? ret : _("Conversation");
2724 const gchar *
2725 empathy_chat_get_subject (EmpathyChat *chat)
2727 EmpathyChatPriv *priv = GET_PRIV (chat);
2729 g_return_val_if_fail (EMPATHY_IS_CHAT (chat), NULL);
2731 return priv->subject;
2734 EmpathyContact *
2735 empathy_chat_get_remote_contact (EmpathyChat *chat)
2737 EmpathyChatPriv *priv = GET_PRIV (chat);
2739 g_return_val_if_fail (EMPATHY_IS_CHAT (chat), NULL);
2741 return priv->remote_contact;
2744 GtkWidget *
2745 empathy_chat_get_contact_menu (EmpathyChat *chat)
2747 EmpathyChatPriv *priv = GET_PRIV (chat);
2748 GtkWidget *menu = NULL;
2750 g_return_val_if_fail (EMPATHY_IS_CHAT (chat), NULL);
2752 if (priv->remote_contact) {
2753 menu = empathy_contact_menu_new (priv->remote_contact,
2754 EMPATHY_CONTACT_FEATURE_CALL |
2755 EMPATHY_CONTACT_FEATURE_LOG |
2756 EMPATHY_CONTACT_FEATURE_INFO);
2758 else if (priv->contact_list_view) {
2759 EmpathyContactListView *view;
2761 view = EMPATHY_CONTACT_LIST_VIEW (priv->contact_list_view);
2762 menu = empathy_contact_list_view_get_contact_menu (view);
2765 return menu;
2768 void
2769 empathy_chat_clear (EmpathyChat *chat)
2771 g_return_if_fail (EMPATHY_IS_CHAT (chat));
2773 empathy_chat_view_clear (chat->view);
2776 void
2777 empathy_chat_scroll_down (EmpathyChat *chat)
2779 g_return_if_fail (EMPATHY_IS_CHAT (chat));
2781 empathy_chat_view_scroll_down (chat->view);
2784 void
2785 empathy_chat_cut (EmpathyChat *chat)
2787 GtkTextBuffer *buffer;
2789 g_return_if_fail (EMPATHY_IS_CHAT (chat));
2791 buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (chat->input_text_view));
2792 if (gtk_text_buffer_get_has_selection (buffer)) {
2793 GtkClipboard *clipboard;
2795 clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD);
2797 gtk_text_buffer_cut_clipboard (buffer, clipboard, TRUE);
2801 void
2802 empathy_chat_copy (EmpathyChat *chat)
2804 GtkTextBuffer *buffer;
2806 g_return_if_fail (EMPATHY_IS_CHAT (chat));
2808 if (empathy_chat_view_get_has_selection (chat->view)) {
2809 empathy_chat_view_copy_clipboard (chat->view);
2810 return;
2813 buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (chat->input_text_view));
2814 if (gtk_text_buffer_get_has_selection (buffer)) {
2815 GtkClipboard *clipboard;
2817 clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD);
2819 gtk_text_buffer_copy_clipboard (buffer, clipboard);
2823 void
2824 empathy_chat_paste (EmpathyChat *chat)
2826 GtkTextBuffer *buffer;
2827 GtkClipboard *clipboard;
2828 EmpathyChatPriv *priv;
2830 g_return_if_fail (EMPATHY_IS_CHAT (chat));
2832 priv = GET_PRIV (chat);
2834 if (priv->tp_chat == NULL ||
2835 !GTK_WIDGET_IS_SENSITIVE (chat->input_text_view))
2836 return;
2838 buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (chat->input_text_view));
2839 clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD);
2841 gtk_text_buffer_paste_clipboard (buffer, clipboard, NULL, TRUE);
2844 void
2845 empathy_chat_correct_word (EmpathyChat *chat,
2846 GtkTextIter *start,
2847 GtkTextIter *end,
2848 const gchar *new_word)
2850 GtkTextBuffer *buffer;
2852 g_return_if_fail (chat != NULL);
2853 g_return_if_fail (new_word != NULL);
2855 buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (chat->input_text_view));
2857 gtk_text_buffer_delete (buffer, start, end);
2858 gtk_text_buffer_insert (buffer, start,
2859 new_word,
2860 -1);
2863 gboolean
2864 empathy_chat_is_room (EmpathyChat *chat)
2866 EmpathyChatPriv *priv = GET_PRIV (chat);
2868 g_return_val_if_fail (EMPATHY_IS_CHAT (chat), FALSE);
2870 return (priv->handle_type == TP_HANDLE_TYPE_ROOM);
2873 guint
2874 empathy_chat_get_nb_unread_messages (EmpathyChat *self)
2876 EmpathyChatPriv *priv = GET_PRIV (self);
2878 g_return_val_if_fail (EMPATHY_IS_CHAT (self), FALSE);
2880 return priv->unread_messages;
2883 /* called when the messages have been read by user */
2884 void
2885 empathy_chat_messages_read (EmpathyChat *self)
2887 EmpathyChatPriv *priv = GET_PRIV (self);
2889 g_return_if_fail (EMPATHY_IS_CHAT (self));
2891 priv->unread_messages = 0;