aboutsummaryrefslogtreecommitdiffstats
path: root/widgets
diff options
context:
space:
mode:
authorMilan Crha <mcrha@redhat.com>2012-05-10 01:50:33 +0800
committerMilan Crha <mcrha@redhat.com>2012-05-10 01:50:33 +0800
commitecc1f7ae88ae9ef26f182f97a481a00470969cd1 (patch)
tree7752611434f9efe7af9982b4307960d97b8fba91 /widgets
parentf46ea07a61ad8fc8a6af8e95fcda73cfa4983348 (diff)
downloadgsoc2013-evolution-ecc1f7ae88ae9ef26f182f97a481a00470969cd1.tar
gsoc2013-evolution-ecc1f7ae88ae9ef26f182f97a481a00470969cd1.tar.gz
gsoc2013-evolution-ecc1f7ae88ae9ef26f182f97a481a00470969cd1.tar.bz2
gsoc2013-evolution-ecc1f7ae88ae9ef26f182f97a481a00470969cd1.tar.lz
gsoc2013-evolution-ecc1f7ae88ae9ef26f182f97a481a00470969cd1.tar.xz
gsoc2013-evolution-ecc1f7ae88ae9ef26f182f97a481a00470969cd1.tar.zst
gsoc2013-evolution-ecc1f7ae88ae9ef26f182f97a481a00470969cd1.zip
Bug #200683 - Composer subject spell checking
Diffstat (limited to 'widgets')
-rw-r--r--widgets/misc/Makefile.am2
-rw-r--r--widgets/misc/e-spell-entry.c861
-rw-r--r--widgets/misc/e-spell-entry.h59
3 files changed, 922 insertions, 0 deletions
diff --git a/widgets/misc/Makefile.am b/widgets/misc/Makefile.am
index ab7efa0667..5768c72bc7 100644
--- a/widgets/misc/Makefile.am
+++ b/widgets/misc/Makefile.am
@@ -60,6 +60,7 @@ widgetsinclude_HEADERS = \
e-signature-preview.h \
e-signature-script-dialog.h \
e-signature-tree-view.h \
+ e-spell-entry.h \
e-url-entry.h \
e-web-view.h \
e-web-view-gtkhtml.h \
@@ -142,6 +143,7 @@ libemiscwidgets_la_SOURCES = \
e-signature-preview.c \
e-signature-script-dialog.c \
e-signature-tree-view.c \
+ e-spell-entry.c \
e-url-entry.c \
e-web-view.c \
e-web-view-gtkhtml.c \
diff --git a/widgets/misc/e-spell-entry.c b/widgets/misc/e-spell-entry.c
new file mode 100644
index 0000000000..1f0e3c9dc4
--- /dev/null
+++ b/widgets/misc/e-spell-entry.c
@@ -0,0 +1,861 @@
+/*
+ * e-spell-entry.c
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with the program; if not, see <http://www.gnu.org/licenses/>
+ *
+ */
+
+/* This code is based on libsexy's SexySpellEntry */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <glib/gi18n-lib.h>
+#include <gtk/gtk.h>
+
+#include <editor/gtkhtml-spell-language.h>
+#include <editor/gtkhtml-spell-checker.h>
+
+#include "e-spell-entry.h"
+
+enum {
+ PROP_0,
+ PROP_CHECKING_ENABLED
+};
+
+struct _ESpellEntryPrivate
+{
+ PangoAttrList *attr_list;
+ gint mark_character;
+ gint entry_scroll_offset;
+ GSettings *settings;
+ gboolean custom_checkers;
+ gboolean checking_enabled;
+ GSList *checkers;
+ gchar **words;
+ gint *word_starts;
+ gint *word_ends;
+};
+
+#define E_SPELL_ENTRY_GET_PRIVATE(obj) \
+ (G_TYPE_INSTANCE_GET_PRIVATE \
+ ((obj), E_TYPE_SPELL_ENTRY, ESpellEntryPrivate))
+
+G_DEFINE_TYPE (ESpellEntry, e_spell_entry, GTK_TYPE_ENTRY);
+
+static gboolean
+word_misspelled (ESpellEntry *entry,
+ gint start,
+ gint end)
+{
+ const gchar *text;
+ gchar *word;
+ gboolean result = TRUE;
+
+ if (start == end)
+ return FALSE;
+
+ text = gtk_entry_get_text (GTK_ENTRY (entry));
+ word = g_new0 (gchar, end - start + 2);
+
+ g_strlcpy (word, text + start, end - start + 1);
+
+ if (g_unichar_isalpha (*word)) {
+ GSList *li;
+ gssize wlen = strlen (word);
+
+ for (li = entry->priv->checkers; li; li = g_slist_next (li)) {
+ GtkhtmlSpellChecker *checker = li->data;
+ if (gtkhtml_spell_checker_check_word (checker, word, wlen)) {
+ result = FALSE;
+ break;
+ }
+ }
+ }
+ g_free (word);
+
+ return result;
+}
+
+static void
+insert_underline (ESpellEntry *entry,
+ guint start,
+ guint end)
+{
+ PangoAttribute *ucolor = pango_attr_underline_color_new (65535, 0, 0);
+ PangoAttribute *unline = pango_attr_underline_new (PANGO_UNDERLINE_ERROR);
+
+ ucolor->start_index = start;
+ unline->start_index = start;
+
+ ucolor->end_index = end;
+ unline->end_index = end;
+
+ pango_attr_list_insert (entry->priv->attr_list, ucolor);
+ pango_attr_list_insert (entry->priv->attr_list, unline);
+}
+
+static void
+check_word (ESpellEntry *entry,
+ gint start,
+ gint end)
+{
+ PangoAttrIterator *it;
+
+ /* Check to see if we've got any attributes at this position.
+ * If so, free them, since we'll readd it if the word is misspelled */
+ it = pango_attr_list_get_iterator (entry->priv->attr_list);
+
+ if (it == NULL)
+ return;
+ do {
+ gint s, e;
+ pango_attr_iterator_range (it, &s, &e);
+ if (s == start) {
+ GSList *attrs = pango_attr_iterator_get_attrs (it);
+ g_slist_foreach (attrs, (GFunc) pango_attribute_destroy, NULL);
+ g_slist_free (attrs);
+ }
+ } while (pango_attr_iterator_next (it));
+ pango_attr_iterator_destroy (it);
+
+ if (word_misspelled (entry, start, end))
+ insert_underline (entry, start, end);
+}
+
+static void
+spell_entry_recheck_all (ESpellEntry *entry)
+{
+ GtkWidget *widget = GTK_WIDGET (entry);
+ PangoLayout *layout;
+ gint length, i;
+
+ /* Remove all existing pango attributes. These will get read as we check */
+ pango_attr_list_unref (entry->priv->attr_list);
+ entry->priv->attr_list = pango_attr_list_new ();
+
+ if (entry->priv->checkers && entry->priv->checking_enabled) {
+ /* Loop through words */
+ for (i = 0; entry->priv->words[i]; i++) {
+ length = strlen (entry->priv->words[i]);
+ if (length == 0)
+ continue;
+ check_word (entry, entry->priv->word_starts[i], entry->priv->word_ends[i]);
+ }
+
+ layout = gtk_entry_get_layout (GTK_ENTRY (entry));
+ pango_layout_set_attributes (layout, entry->priv->attr_list);
+ }
+
+ if (gtk_widget_get_realized (widget))
+ gtk_widget_queue_draw (widget);
+}
+
+static void
+get_word_extents_from_position (ESpellEntry *entry,
+ gint *start,
+ gint *end,
+ guint position)
+{
+ const gchar *text;
+ gint i, bytes_pos;
+
+ *start = -1;
+ *end = -1;
+
+ if (entry->priv->words == NULL)
+ return;
+
+ text = gtk_entry_get_text (GTK_ENTRY (entry));
+ bytes_pos = (gint) (g_utf8_offset_to_pointer (text, position) - text);
+
+ for (i = 0; entry->priv->words[i]; i++) {
+ if (bytes_pos >= entry->priv->word_starts[i] &&
+ bytes_pos <= entry->priv->word_ends[i]) {
+ *start = entry->priv->word_starts[i];
+ *end = entry->priv->word_ends[i];
+ return;
+ }
+ }
+}
+
+static void
+entry_strsplit_utf8 (GtkEntry *entry,
+ gchar ***set,
+ gint **starts,
+ gint **ends)
+{
+ PangoLayout *layout;
+ PangoLogAttr *log_attrs;
+ const gchar *text;
+ gint n_attrs, n_strings, i, j;
+
+ layout = gtk_entry_get_layout (GTK_ENTRY (entry));
+ text = gtk_entry_get_text (GTK_ENTRY (entry));
+ pango_layout_get_log_attrs (layout, &log_attrs, &n_attrs);
+
+ /* Find how many words we have */
+ n_strings = 0;
+ for (i = 0; i < n_attrs; i++)
+ if (log_attrs[i].is_word_start)
+ n_strings++;
+
+ *set = g_new0 (gchar *, n_strings + 1);
+ *starts = g_new0 (gint, n_strings);
+ *ends = g_new0 (gint, n_strings);
+
+ /* Copy out strings */
+ for (i = 0, j = 0; i < n_attrs; i++) {
+ if (log_attrs[i].is_word_start) {
+ gint cend, bytes;
+ gchar *start;
+
+ /* Find the end of this string */
+ cend = i;
+ while (!(log_attrs[cend].is_word_end))
+ cend++;
+
+ /* Copy sub-string */
+ start = g_utf8_offset_to_pointer (text, i);
+ bytes = (gint) (g_utf8_offset_to_pointer (text, cend) - start);
+ (*set)[j] = g_new0 (gchar, bytes + 1);
+ (*starts)[j] = (gint) (start - text);
+ (*ends)[j] = (gint) (start - text + bytes);
+ g_utf8_strncpy ((*set)[j], start, cend - i);
+
+ /* Move on to the next word */
+ j++;
+ }
+ }
+
+ g_free (log_attrs);
+}
+
+static void
+add_to_dictionary (GtkWidget *menuitem,
+ ESpellEntry *entry)
+{
+ gchar *word;
+ gint start, end;
+ GtkhtmlSpellChecker *checker;
+
+ get_word_extents_from_position (entry, &start, &end, entry->priv->mark_character);
+ word = gtk_editable_get_chars (GTK_EDITABLE (entry), start, end);
+
+ checker = g_object_get_data (G_OBJECT (menuitem), "spell-entry-checker");
+ if (checker)
+ gtkhtml_spell_checker_add_word (checker, word, -1);
+
+ g_free (word);
+
+ if (entry->priv->words) {
+ g_strfreev (entry->priv->words);
+ g_free (entry->priv->word_starts);
+ g_free (entry->priv->word_ends);
+ }
+
+ entry_strsplit_utf8 (GTK_ENTRY (entry), &entry->priv->words, &entry->priv->word_starts, &entry->priv->word_ends);
+ spell_entry_recheck_all (entry);
+}
+
+static void
+ignore_all (GtkWidget *menuitem,
+ ESpellEntry *entry)
+{
+ gchar *word;
+ gint start, end;
+ GSList *li;
+
+ get_word_extents_from_position (entry, &start, &end, entry->priv->mark_character);
+ word = gtk_editable_get_chars (GTK_EDITABLE (entry), start, end);
+
+ for (li = entry->priv->checkers; li; li = g_slist_next (li)) {
+ GtkhtmlSpellChecker *checker = li->data;
+ gtkhtml_spell_checker_add_word_to_session (checker, word, -1);
+ }
+
+ g_free (word);
+
+ if (entry->priv->words) {
+ g_strfreev (entry->priv->words);
+ g_free (entry->priv->word_starts);
+ g_free (entry->priv->word_ends);
+ }
+ entry_strsplit_utf8 (GTK_ENTRY (entry), &entry->priv->words, &entry->priv->word_starts, &entry->priv->word_ends);
+ spell_entry_recheck_all (entry);
+}
+
+static void
+replace_word (GtkWidget *menuitem,
+ ESpellEntry *entry)
+{
+ gchar *oldword;
+ const gchar *newword;
+ gint start, end;
+ gint cursor;
+ GtkhtmlSpellChecker *checker;
+
+ get_word_extents_from_position (entry, &start, &end, entry->priv->mark_character);
+ oldword = gtk_editable_get_chars (GTK_EDITABLE (entry), start, end);
+ newword = gtk_label_get_text (GTK_LABEL (gtk_bin_get_child (GTK_BIN (menuitem))));
+
+ cursor = gtk_editable_get_position (GTK_EDITABLE (entry));
+ /* is the cursor at the end? If so, restore it there */
+ if (g_utf8_strlen (gtk_entry_get_text (GTK_ENTRY (entry)), -1) == cursor)
+ cursor = -1;
+ else if (cursor > start && cursor <= end)
+ cursor = start;
+
+ gtk_editable_delete_text (GTK_EDITABLE (entry), start, end);
+ gtk_editable_set_position (GTK_EDITABLE (entry), start);
+ gtk_editable_insert_text (GTK_EDITABLE (entry), newword, strlen (newword),
+ &start);
+ gtk_editable_set_position (GTK_EDITABLE (entry), cursor);
+
+ checker = g_object_get_data (G_OBJECT (menuitem), "spell-entry-checker");
+
+ if (checker)
+ gtkhtml_spell_checker_store_replacement (checker, oldword, -1, newword, -1);
+
+ g_free (oldword);
+}
+
+static void
+build_suggestion_menu (ESpellEntry *entry,
+ GtkWidget *menu,
+ GtkhtmlSpellChecker *checker,
+ const gchar *word)
+{
+ GtkWidget *mi;
+ GList *suggestions, *iter;
+
+ suggestions = gtkhtml_spell_checker_get_suggestions (checker, word, -1);
+
+ if (!suggestions) {
+ /* no suggestions. Put something in the menu anyway... */
+ GtkWidget *label = gtk_label_new (_("(no suggestions)"));
+ PangoAttribute *attribute;
+ PangoAttrList *attribute_list;
+
+ attribute_list = pango_attr_list_new ();
+ attribute = pango_attr_style_new (PANGO_STYLE_ITALIC);
+ pango_attr_list_insert (attribute_list, attribute);
+ gtk_label_set_attributes (GTK_LABEL (label), attribute_list);
+ pango_attr_list_unref (attribute_list);
+
+ mi = gtk_separator_menu_item_new ();
+ gtk_container_add (GTK_CONTAINER (mi), label);
+ gtk_widget_show_all (mi);
+ gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), mi);
+ } else {
+ gint ii = 0;
+
+ /* build a set of menus with suggestions */
+ for (iter = suggestions; iter; iter = g_list_next (iter), ii++) {
+ if ((ii != 0) && (ii % 10 == 0)) {
+ mi = gtk_separator_menu_item_new ();
+ gtk_widget_show (mi);
+ gtk_menu_shell_append (GTK_MENU_SHELL (menu), mi);
+
+ mi = gtk_menu_item_new_with_label (_("More..."));
+ gtk_widget_show (mi);
+ gtk_menu_shell_append (GTK_MENU_SHELL (menu), mi);
+
+ menu = gtk_menu_new ();
+ gtk_menu_item_set_submenu (GTK_MENU_ITEM (mi), menu);
+ }
+
+ mi = gtk_menu_item_new_with_label (iter->data);
+ g_object_set_data (G_OBJECT (mi), "spell-entry-checker", checker);
+ g_signal_connect (G_OBJECT (mi), "activate", G_CALLBACK (replace_word), entry);
+ gtk_widget_show (mi);
+ gtk_menu_shell_append (GTK_MENU_SHELL (menu), mi);
+ }
+ }
+
+ g_list_free_full (suggestions, g_free);
+}
+
+static GtkWidget *
+build_spelling_menu (ESpellEntry *entry,
+ const gchar *word)
+{
+ GtkhtmlSpellChecker *checker;
+ GtkWidget *topmenu, *mi;
+ gchar *label;
+
+ topmenu = gtk_menu_new ();
+
+ if (!entry->priv->checkers)
+ return topmenu;
+
+ /* Suggestions */
+ if (!entry->priv->checkers->next) {
+ checker = entry->priv->checkers->data;
+ build_suggestion_menu (entry, topmenu, checker, word);
+ } else {
+ GSList *li;
+ GtkWidget *menu;
+ const gchar *lang_name;
+
+ for (li = entry->priv->checkers; li; li = g_slist_next (li)) {
+ const GtkhtmlSpellLanguage *language;
+
+ checker = li->data;
+ language = gtkhtml_spell_checker_get_language (checker);
+ if (!language)
+ continue;
+
+ lang_name = gtkhtml_spell_language_get_name (language);
+ if (!lang_name)
+ lang_name = gtkhtml_spell_language_get_code (language);
+
+ mi = gtk_menu_item_new_with_label (lang_name ? lang_name : "???");
+
+ gtk_widget_show (mi);
+ gtk_menu_shell_append (GTK_MENU_SHELL (topmenu), mi);
+ menu = gtk_menu_new ();
+ gtk_menu_item_set_submenu (GTK_MENU_ITEM (mi), menu);
+ build_suggestion_menu (entry, menu, checker, word);
+ }
+ }
+
+ /* Separator */
+ mi = gtk_separator_menu_item_new ();
+ gtk_widget_show (mi);
+ gtk_menu_shell_append (GTK_MENU_SHELL (topmenu), mi);
+
+ /* + Add to Dictionary */
+ label = g_strdup_printf (_("Add \"%s\" to Dictionary"), word);
+ mi = gtk_image_menu_item_new_with_label (label);
+ g_free (label);
+
+ gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (mi), gtk_image_new_from_stock (GTK_STOCK_ADD, GTK_ICON_SIZE_MENU));
+
+ if (!entry->priv->checkers->next) {
+ checker = entry->priv->checkers->data;
+ g_object_set_data (G_OBJECT (mi), "spell-entry-checker", checker);
+ g_signal_connect (G_OBJECT (mi), "activate", G_CALLBACK (add_to_dictionary), entry);
+ } else {
+ GSList *li;
+ GtkWidget *menu, *submi;
+ const gchar *lang_name;
+
+ menu = gtk_menu_new ();
+ gtk_menu_item_set_submenu (GTK_MENU_ITEM (mi), menu);
+
+ for (li = entry->priv->checkers; li; li = g_slist_next (li)) {
+ const GtkhtmlSpellLanguage *language;
+
+ checker = li->data;
+ language = gtkhtml_spell_checker_get_language (checker);
+ if (!language)
+ continue;
+
+ lang_name = gtkhtml_spell_language_get_name (language);
+ if (!lang_name)
+ lang_name = gtkhtml_spell_language_get_code (language);
+
+ submi = gtk_menu_item_new_with_label (lang_name ? lang_name : "???");
+ g_object_set_data (G_OBJECT (submi), "spell-entry-checker", checker);
+ g_signal_connect (G_OBJECT (submi), "activate", G_CALLBACK (add_to_dictionary), entry);
+
+ gtk_widget_show (submi);
+ gtk_menu_shell_append (GTK_MENU_SHELL (menu), submi);
+ }
+ }
+
+ gtk_widget_show_all (mi);
+ gtk_menu_shell_append (GTK_MENU_SHELL (topmenu), mi);
+
+ /* - Ignore All */
+ mi = gtk_image_menu_item_new_with_label (_("Ignore All"));
+ gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (mi), gtk_image_new_from_stock (GTK_STOCK_REMOVE, GTK_ICON_SIZE_MENU));
+ g_signal_connect (G_OBJECT (mi), "activate", G_CALLBACK (ignore_all), entry);
+ gtk_widget_show_all (mi);
+ gtk_menu_shell_append (GTK_MENU_SHELL (topmenu), mi);
+
+ return topmenu;
+}
+
+static void
+spell_entry_add_suggestions_menu (ESpellEntry *entry,
+ GtkMenu *menu,
+ const gchar *word)
+{
+ GtkWidget *icon, *mi;
+
+ g_return_if_fail (menu != NULL);
+ g_return_if_fail (word != NULL);
+
+ /* separator */
+ mi = gtk_separator_menu_item_new ();
+ gtk_widget_show (mi);
+ gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), mi);
+
+ /* Above the separator, show the suggestions menu */
+ icon = gtk_image_new_from_stock (GTK_STOCK_SPELL_CHECK, GTK_ICON_SIZE_MENU);
+ mi = gtk_image_menu_item_new_with_label(_("Spelling Suggestions"));
+ gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (mi), icon);
+
+ gtk_menu_item_set_submenu (GTK_MENU_ITEM (mi), build_spelling_menu (entry, word));
+
+ gtk_widget_show_all (mi);
+ gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), mi);
+}
+
+static gboolean
+spell_entry_popup_menu (ESpellEntry *entry)
+{
+ /* Menu popped up from a keybinding (menu key or <shift>+F10). Use
+ * the cursor position as the mark position */
+ entry->priv->mark_character = gtk_editable_get_position (GTK_EDITABLE (entry));
+
+ return FALSE;
+}
+
+static void
+spell_entry_populate_popup (ESpellEntry *entry,
+ GtkMenu *menu,
+ gpointer data)
+{
+ gint start, end;
+ gchar *word;
+
+ if (!entry->priv->checkers)
+ return;
+
+ get_word_extents_from_position (entry, &start, &end, entry->priv->mark_character);
+ if (start == end)
+ return;
+
+ if (!word_misspelled (entry, start, end))
+ return;
+
+ word = gtk_editable_get_chars (GTK_EDITABLE (entry), start, end);
+ g_return_if_fail (word != NULL);
+
+ spell_entry_add_suggestions_menu (entry, menu, word);
+
+ g_free (word);
+}
+
+static void
+spell_entry_changed (GtkEditable *editable)
+{
+ ESpellEntry *entry = E_SPELL_ENTRY (editable);
+
+ if (!entry->priv->checkers)
+ return;
+
+ if (entry->priv->words) {
+ g_strfreev (entry->priv->words);
+ g_free (entry->priv->word_starts);
+ g_free (entry->priv->word_ends);
+ }
+ entry_strsplit_utf8 (GTK_ENTRY (entry), &entry->priv->words, &entry->priv->word_starts, &entry->priv->word_ends);
+ spell_entry_recheck_all (entry);
+}
+
+static void
+spell_entry_notify_scroll_offset (ESpellEntry *spell_entry)
+{
+ g_return_if_fail (spell_entry != NULL);
+
+ g_object_get (G_OBJECT (spell_entry), "scroll-offset", &spell_entry->priv->entry_scroll_offset, NULL);
+}
+
+static GList *
+spell_entry_load_spell_languages (void)
+{
+ GSettings *settings;
+ GList *spell_languages = NULL;
+ gchar **strv;
+ gint ii;
+
+ /* Ask GSettings for a list of spell check language codes. */
+ settings = g_settings_new ("org.gnome.evolution.mail");
+ strv = g_settings_get_strv (settings, "composer-spell-languages");
+ g_object_unref (settings);
+
+ /* Convert the codes to spell language structs. */
+ for (ii = 0; strv[ii] != NULL; ii++) {
+ gchar *language_code = strv[ii];
+ const GtkhtmlSpellLanguage *language;
+
+ language = gtkhtml_spell_language_lookup (language_code);
+ if (language != NULL)
+ spell_languages = g_list_prepend (
+ spell_languages, (gpointer) language);
+ }
+
+ g_strfreev (strv);
+
+ spell_languages = g_list_reverse (spell_languages);
+
+ /* Pick a default spell language if it came back empty. */
+ if (spell_languages == NULL) {
+ const GtkhtmlSpellLanguage *language;
+
+ language = gtkhtml_spell_language_lookup (NULL);
+
+ if (language) {
+ spell_languages = g_list_prepend (
+ spell_languages, (gpointer) language);
+ }
+ }
+
+ return spell_languages;
+}
+
+static void
+spell_entry_settings_changed (ESpellEntry *spell_entry,
+ const gchar *key)
+{
+ GList *languages;
+
+ g_return_if_fail (spell_entry != NULL);
+
+ if (spell_entry->priv->custom_checkers)
+ return;
+
+ if (key && !g_str_equal (key, "composer-spell-languages"))
+ return;
+
+ languages = spell_entry_load_spell_languages ();
+ e_spell_entry_set_languages (spell_entry, languages);
+ g_list_free (languages);
+
+ spell_entry->priv->custom_checkers = FALSE;
+}
+
+static gint
+spell_entry_find_position (ESpellEntry *spell_entry,
+ gint x)
+{
+ PangoLayout *layout;
+ PangoLayoutLine *line;
+ gint index;
+ gint pos;
+ gint trailing;
+ const gchar *text;
+ GtkEntry *entry = GTK_ENTRY (spell_entry);
+
+ layout = gtk_entry_get_layout (entry);
+ text = pango_layout_get_text (layout);
+
+ line = pango_layout_get_lines_readonly (layout)->data;
+ pango_layout_line_x_to_index (line, x * PANGO_SCALE, &index, &trailing);
+
+ pos = g_utf8_pointer_to_offset (text, text + index);
+ pos += trailing;
+
+ return pos;
+}
+
+static gboolean
+e_spell_entry_draw (GtkWidget *widget,
+ cairo_t *cr)
+{
+ ESpellEntry *spell_entry = E_SPELL_ENTRY (widget);
+ GtkEntry *entry = GTK_ENTRY (widget);
+ PangoLayout *layout;
+
+ layout = gtk_entry_get_layout (entry);
+ pango_layout_set_attributes (layout, spell_entry->priv->attr_list);
+
+ return GTK_WIDGET_CLASS (e_spell_entry_parent_class)->draw (widget, cr);
+}
+
+static gboolean
+e_spell_entry_button_press (GtkWidget *widget,
+ GdkEventButton *event)
+{
+ ESpellEntry *spell_entry = E_SPELL_ENTRY (widget);
+
+ spell_entry->priv->mark_character = spell_entry_find_position (
+ spell_entry, event->x + spell_entry->priv->entry_scroll_offset);
+
+ return GTK_WIDGET_CLASS (e_spell_entry_parent_class)->button_press_event (widget, event);
+}
+
+static void
+spell_entry_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ switch (property_id) {
+ case PROP_CHECKING_ENABLED:
+ e_spell_entry_set_checking_enabled (
+ E_SPELL_ENTRY (object),
+ g_value_get_boolean (value));
+ return;
+ }
+
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+spell_entry_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ switch (property_id) {
+ case PROP_CHECKING_ENABLED:
+ g_value_set_boolean (
+ value,
+ e_spell_entry_get_checking_enabled (
+ E_SPELL_ENTRY (object)));
+ return;
+ }
+
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+e_spell_entry_init (ESpellEntry *spell_entry)
+{
+ spell_entry->priv = E_SPELL_ENTRY_GET_PRIVATE (spell_entry);
+ spell_entry->priv->attr_list = pango_attr_list_new ();
+ spell_entry->priv->checkers = NULL;
+ spell_entry->priv->checking_enabled = TRUE;
+
+ g_signal_connect (spell_entry, "popup-menu", G_CALLBACK (spell_entry_popup_menu), NULL);
+ g_signal_connect (spell_entry, "populate-popup", G_CALLBACK (spell_entry_populate_popup), NULL);
+ g_signal_connect (spell_entry, "changed", G_CALLBACK (spell_entry_changed), NULL);
+ g_signal_connect (spell_entry, "notify::scroll-offset", G_CALLBACK (spell_entry_notify_scroll_offset), NULL);
+
+ /* listen for languages changes */
+ spell_entry->priv->settings = g_settings_new ("org.gnome.evolution.mail");
+ g_signal_connect_swapped (spell_entry->priv->settings, "changed", G_CALLBACK (spell_entry_settings_changed), spell_entry);
+
+ /* load current settings */
+ spell_entry_settings_changed (spell_entry, NULL);
+}
+
+static void
+e_spell_entry_finalize (GObject *object)
+{
+ ESpellEntry *entry;
+
+ g_return_if_fail (object != NULL);
+ g_return_if_fail (E_IS_SPELL_ENTRY (object));
+
+ entry = E_SPELL_ENTRY (object);
+
+ if (entry->priv->settings)
+ g_object_unref (entry->priv->settings);
+ if (entry->priv->checkers)
+ g_slist_free_full (entry->priv->checkers, g_object_unref);
+ if (entry->priv->attr_list)
+ pango_attr_list_unref (entry->priv->attr_list);
+ if (entry->priv->words)
+ g_strfreev (entry->priv->words);
+ if (entry->priv->word_starts)
+ g_free (entry->priv->word_starts);
+ if (entry->priv->word_ends)
+ g_free (entry->priv->word_ends);
+
+ G_OBJECT_CLASS (e_spell_entry_parent_class)->finalize (object);
+}
+
+static void
+e_spell_entry_class_init (ESpellEntryClass *klass)
+{
+ GObjectClass *object_class;
+ GtkWidgetClass *widget_class;
+
+ g_type_class_add_private (klass, sizeof (ESpellEntryPrivate));
+
+ object_class = G_OBJECT_CLASS (klass);
+ object_class->set_property = spell_entry_set_property;
+ object_class->get_property = spell_entry_get_property;
+ object_class->finalize = e_spell_entry_finalize;
+
+ widget_class = GTK_WIDGET_CLASS (klass);
+ widget_class->draw = e_spell_entry_draw;
+ widget_class->button_press_event = e_spell_entry_button_press;
+
+ g_object_class_install_property (
+ object_class,
+ PROP_CHECKING_ENABLED,
+ g_param_spec_boolean (
+ "checking-enabled",
+ "checking enabled",
+ "Spell Checking is Enabled",
+ TRUE,
+ G_PARAM_READWRITE));
+}
+
+GtkWidget *
+e_spell_entry_new (void)
+{
+ return g_object_new (E_TYPE_SPELL_ENTRY, NULL);
+}
+
+/* 'languages' consists of 'const GtkhtmlSpellLanguage *' */
+void
+e_spell_entry_set_languages (ESpellEntry *spell_entry,
+ GList *languages)
+{
+ GList *iter;
+
+ g_return_if_fail (spell_entry != NULL);
+
+ spell_entry->priv->custom_checkers = TRUE;
+
+ if (spell_entry->priv->checkers)
+ g_slist_free_full (spell_entry->priv->checkers, g_object_unref);
+ spell_entry->priv->checkers = NULL;
+
+ for (iter = languages; iter; iter = g_list_next (iter)) {
+ const GtkhtmlSpellLanguage *language = iter->data;
+
+ if (language)
+ spell_entry->priv->checkers = g_slist_prepend (spell_entry->priv->checkers,
+ gtkhtml_spell_checker_new (language));
+ }
+
+ spell_entry->priv->checkers = g_slist_reverse (spell_entry->priv->checkers);
+
+ if (gtk_widget_get_realized (GTK_WIDGET (spell_entry)))
+ spell_entry_recheck_all (spell_entry);
+}
+
+gboolean
+e_spell_entry_get_checking_enabled (ESpellEntry *spell_entry)
+{
+ g_return_val_if_fail (spell_entry != NULL, FALSE);
+
+ return spell_entry->priv->checking_enabled;
+}
+
+void
+e_spell_entry_set_checking_enabled (ESpellEntry *spell_entry,
+ gboolean enable_checking)
+{
+ g_return_if_fail (spell_entry != NULL);
+
+ if ((enable_checking ? 1 : 0) == (spell_entry->priv->checking_enabled ? 1 : 0))
+ return;
+
+ spell_entry->priv->checking_enabled = enable_checking;
+ spell_entry_recheck_all (spell_entry);
+
+ g_object_notify (G_OBJECT (spell_entry), "checking-enabled");
+
+}
diff --git a/widgets/misc/e-spell-entry.h b/widgets/misc/e-spell-entry.h
new file mode 100644
index 0000000000..2d6aabad95
--- /dev/null
+++ b/widgets/misc/e-spell-entry.h
@@ -0,0 +1,59 @@
+/*
+ * e-spell-entry.h
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with the program; if not, see <http://www.gnu.org/licenses/>
+ *
+ */
+
+#ifndef E_SPELL_ENTRY_H
+#define E_SPELL_ENTRY_H
+
+#include <gtk/gtk.h>
+
+#define E_TYPE_SPELL_ENTRY (e_spell_entry_get_type())
+#define E_SPELL_ENTRY(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), E_TYPE_SPELL_ENTRY, ESpellEntry))
+#define E_SPELL_ENTRY_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), E_TYPE_SPELL_ENTRY, ESpellEntryClass))
+#define E_IS_SPELL_ENTRY(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), E_TYPE_SPELL_ENTRY))
+#define E_IS_SPELL_ENTRY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), E_TYPE_SPELL_ENTRY))
+#define E_SPELL_ENTRY_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), E_TYPE_SPELL_ENTRY, ESpellEntryClass))
+
+G_BEGIN_DECLS
+
+typedef struct _ESpellEntry ESpellEntry;
+typedef struct _ESpellEntryClass ESpellEntryClass;
+typedef struct _ESpellEntryPrivate ESpellEntryPrivate;
+
+struct _ESpellEntry
+{
+ GtkEntry parent_object;
+
+ ESpellEntryPrivate *priv;
+};
+
+struct _ESpellEntryClass
+{
+ GtkEntryClass parent_class;
+};
+
+GType e_spell_entry_get_type (void);
+GtkWidget * e_spell_entry_new (void);
+void e_spell_entry_set_languages (ESpellEntry *spell_entry,
+ GList *languages);
+gboolean e_spell_entry_get_checking_enabled (ESpellEntry *spell_entry);
+void e_spell_entry_set_checking_enabled (ESpellEntry *spell_entry,
+ gboolean enable_checking);
+
+G_END_DECLS
+
+#endif /* E_SPELL_ENTRY_H */