[evolution] Bug #200683 - Composer subject spell checking



commit ecc1f7ae88ae9ef26f182f97a481a00470969cd1
Author: Milan Crha <mcrha redhat com>
Date:   Wed May 9 19:50:33 2012 +0200

    Bug #200683 - Composer subject spell checking

 composer/Makefile.am               |    2 +
 composer/e-composer-header-table.c |    3 +-
 composer/e-composer-private.c      |   23 +
 composer/e-composer-spell-header.c |   75 ++++
 composer/e-composer-spell-header.h |   68 +++
 composer/e-composer-text-header.c  |   31 +-
 composer/e-composer-text-header.h  |    2 +
 configure.ac                       |    2 +-
 widgets/misc/Makefile.am           |    2 +
 widgets/misc/e-spell-entry.c       |  861 ++++++++++++++++++++++++++++++++++++
 widgets/misc/e-spell-entry.h       |   59 +++
 11 files changed, 1119 insertions(+), 9 deletions(-)
---
diff --git a/composer/Makefile.am b/composer/Makefile.am
index c3ea743..59b3160 100644
--- a/composer/Makefile.am
+++ b/composer/Makefile.am
@@ -18,6 +18,7 @@ libcomposerinclude_HEADERS = 			\
 	e-composer-name-header.h		\
 	e-composer-post-header.h		\
 	e-composer-private.h			\
+	e-composer-spell-header.h		\
 	e-composer-text-header.h		\
 	e-msg-composer.h
 
@@ -51,6 +52,7 @@ libcomposer_la_SOURCES = 			\
 	e-composer-name-header.c		\
 	e-composer-post-header.c		\
 	e-composer-private.c			\
+	e-composer-spell-header.c		\
 	e-composer-text-header.c		\
 	e-msg-composer.c
 
diff --git a/composer/e-composer-header-table.c b/composer/e-composer-header-table.c
index f2a9356..51a217d 100644
--- a/composer/e-composer-header-table.c
+++ b/composer/e-composer-header-table.c
@@ -32,6 +32,7 @@
 #include "e-composer-from-header.h"
 #include "e-composer-name-header.h"
 #include "e-composer-post-header.h"
+#include "e-composer-spell-header.h"
 #include "e-composer-text-header.h"
 
 #define E_COMPOSER_HEADER_TABLE_GET_PRIVATE(obj) \
@@ -808,7 +809,7 @@ composer_header_table_constructed (GObject *object)
 	composer_header_table_bind_header ("post-to", "changed", header);
 	table->priv->headers[E_COMPOSER_HEADER_POST_TO] = header;
 
-	header = e_composer_text_header_new_label (_("S_ubject:"));
+	header = e_composer_spell_header_new_label (_("S_ubject:"));
 	composer_header_table_bind_header ("subject", "changed", header);
 	table->priv->headers[E_COMPOSER_HEADER_SUBJECT] = header;
 
diff --git a/composer/e-composer-private.c b/composer/e-composer-private.c
index 477aae4..d5e9756 100644
--- a/composer/e-composer-private.c
+++ b/composer/e-composer-private.c
@@ -22,6 +22,7 @@
 #endif
 
 #include "e-composer-private.h"
+#include "e-composer-spell-header.h"
 #include "e-util/e-util-private.h"
 
 /* Initial height of the picture gallery. */
@@ -122,6 +123,17 @@ composer_update_gallery_visibility (EMsgComposer *composer)
 	}
 }
 
+static void
+composer_spell_languages_changed (EMsgComposer *composer,
+				  GList *languages)
+{
+	EComposerHeader *header;
+	EComposerHeaderTable *table = e_msg_composer_get_header_table (composer);
+
+	header = e_composer_header_table_get_header (table, E_COMPOSER_HEADER_SUBJECT);
+	e_composer_spell_header_set_languages (E_COMPOSER_SPELL_HEADER (header), languages);
+}
+
 void
 e_composer_private_constructed (EMsgComposer *composer)
 {
@@ -142,6 +154,7 @@ e_composer_private_constructed (EMsgComposer *composer)
 	gchar *filename, *gallery_path;
 	gint ii;
 	GError *error = NULL;
+	EComposerHeader *header;
 
 	editor = GTKHTML_EDITOR (composer);
 	ui_manager = gtkhtml_editor_get_ui_manager (editor);
@@ -280,6 +293,16 @@ e_composer_private_constructed (EMsgComposer *composer)
 	priv->header_table = g_object_ref (widget);
 	gtk_widget_show (widget);
 
+	header = e_composer_header_table_get_header (E_COMPOSER_HEADER_TABLE (widget),
+		E_COMPOSER_HEADER_SUBJECT);
+	g_object_bind_property (
+		shell_settings, "composer-inline-spelling",
+		header->input_widget, "checking-enabled",
+		G_BINDING_SYNC_CREATE);
+
+	g_signal_connect (G_OBJECT (composer), "spell-languages-changed",
+		G_CALLBACK (composer_spell_languages_changed), NULL);
+
 	/* Construct the attachment paned. */
 
 	if (small_screen_mode) {
diff --git a/composer/e-composer-spell-header.c b/composer/e-composer-spell-header.c
new file mode 100644
index 0000000..223f863
--- /dev/null
+++ b/composer/e-composer-spell-header.c
@@ -0,0 +1,75 @@
+/*
+ * 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/>
+ *
+ * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <misc/e-spell-entry.h>
+
+#include "e-composer-spell-header.h"
+
+G_DEFINE_TYPE (
+	EComposerSpellHeader,
+	e_composer_spell_header,
+	E_TYPE_COMPOSER_TEXT_HEADER)
+
+static void
+e_composer_spell_header_class_init (EComposerSpellHeaderClass *class)
+{
+	EComposerTextHeaderClass *composer_text_header_class;
+
+	composer_text_header_class = E_COMPOSER_TEXT_HEADER_CLASS (class);
+	composer_text_header_class->entry_type = E_TYPE_SPELL_ENTRY;
+}
+
+static void
+e_composer_spell_header_init (EComposerSpellHeader *header)
+{
+}
+
+EComposerHeader *
+e_composer_spell_header_new_label (const gchar *label)
+{
+	return g_object_new (
+		E_TYPE_COMPOSER_SPELL_HEADER,
+		"label", label, "button", FALSE,
+		NULL);
+}
+
+EComposerHeader *
+e_composer_spell_header_new_button (const gchar *label)
+{
+	return g_object_new (
+		E_TYPE_COMPOSER_SPELL_HEADER,
+		"label", label, "button", TRUE,
+		NULL);
+}
+
+void
+e_composer_spell_header_set_languages (EComposerSpellHeader *spell_header,
+				       GList *languages)
+{
+	ESpellEntry *spell_entry;
+
+	g_return_if_fail (spell_header != NULL);
+
+	spell_entry = E_SPELL_ENTRY (E_COMPOSER_HEADER (spell_header)->input_widget);
+	g_return_if_fail (spell_entry != NULL);
+
+	e_spell_entry_set_languages (spell_entry, languages);
+}
diff --git a/composer/e-composer-spell-header.h b/composer/e-composer-spell-header.h
new file mode 100644
index 0000000..44ac8d1
--- /dev/null
+++ b/composer/e-composer-spell-header.h
@@ -0,0 +1,68 @@
+/*
+ *
+ * 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/>
+ *
+ *
+ * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
+ *
+ */
+
+#ifndef E_COMPOSER_SPELL_HEADER_H
+#define E_COMPOSER_SPELL_HEADER_H
+
+#include <composer/e-composer-text-header.h>
+
+/* Standard GObject macros */
+#define E_TYPE_COMPOSER_SPELL_HEADER \
+	(e_composer_spell_header_get_type ())
+#define E_COMPOSER_SPELL_HEADER(obj) \
+	(G_TYPE_CHECK_INSTANCE_CAST \
+	((obj), E_TYPE_COMPOSER_SPELL_HEADER, EComposerSpellHeader))
+#define E_COMPOSER_SPELL_HEADER_CLASS(cls) \
+	(G_TYPE_CHECK_CLASS_CAST \
+	((cls), E_TYPE_COMPOSER_SPELL_HEADER, EComposerSpellHeaderClass))
+#define E_IS_COMPOSER_SPELL_HEADER(obj) \
+	(G_TYPE_CHECK_INSTANCE_TYPE \
+	((obj), E_TYPE_COMPOSER_SPELL_HEADER))
+#define E_IS_COMPOSER_SPELL_HEADER_CLASS(cls) \
+	(G_TYPE_CHECK_CLASS_TYPE \
+	((cls), E_TYPE_COMPOSER_SPELL_HEADER))
+#define E_COMPOSER_SPELL_HEADER_GET_CLASS(obj) \
+	(G_TYPE_INSTANCE_GET_CLASS \
+	((obj), E_TYPE_COMPOSER_SPELL_HEADER, EComposerSpellHeaderClass))
+
+G_BEGIN_DECLS
+
+typedef struct _EComposerSpellHeader EComposerSpellHeader;
+typedef struct _EComposerSpellHeaderClass EComposerSpellHeaderClass;
+
+struct _EComposerSpellHeader {
+	EComposerTextHeader parent;
+};
+
+struct _EComposerSpellHeaderClass {
+	EComposerTextHeaderClass parent_class;
+
+	GType entry_type;
+};
+
+GType			e_composer_spell_header_get_type	(void);
+EComposerHeader *	e_composer_spell_header_new_label	(const gchar *label);
+EComposerHeader *	e_composer_spell_header_new_button	(const gchar *label);
+void			e_composer_spell_header_set_languages	(EComposerSpellHeader *spell_header,
+								 GList *languages);
+
+G_END_DECLS
+
+#endif /* E_COMPOSER_SPELL_HEADER_H */
diff --git a/composer/e-composer-text-header.c b/composer/e-composer-text-header.c
index 3927923..8506541 100644
--- a/composer/e-composer-text-header.c
+++ b/composer/e-composer-text-header.c
@@ -66,16 +66,17 @@ composer_text_header_query_tooltip_cb (GtkEntry *entry,
 }
 
 static void
-e_composer_text_header_class_init (EComposerTextHeaderClass *class)
-{
-}
-
-static void
-e_composer_text_header_init (EComposerTextHeader *header)
+e_composer_text_header_constructed (GObject *object)
 {
 	GtkWidget *widget;
+	EComposerTextHeader *header;
+
+	G_OBJECT_CLASS (e_composer_text_header_parent_class)->constructed (object);
+
+	header = E_COMPOSER_TEXT_HEADER (object);
+	g_return_if_fail (header != NULL);
 
-	widget = g_object_ref_sink (gtk_entry_new ());
+	widget = g_object_ref_sink (g_object_new (E_COMPOSER_TEXT_HEADER_GET_CLASS (header)->entry_type, NULL));
 	g_signal_connect (
 		widget, "changed",
 		G_CALLBACK (composer_text_header_changed_cb), header);
@@ -86,6 +87,22 @@ e_composer_text_header_init (EComposerTextHeader *header)
 	E_COMPOSER_HEADER (header)->input_widget = widget;
 }
 
+static void
+e_composer_text_header_class_init (EComposerTextHeaderClass *class)
+{
+	GObjectClass *object_class;
+
+	class->entry_type = GTK_TYPE_ENTRY;
+
+	object_class = G_OBJECT_CLASS (class);
+	object_class->constructed = e_composer_text_header_constructed;
+}
+
+static void
+e_composer_text_header_init (EComposerTextHeader *header)
+{
+}
+
 EComposerHeader *
 e_composer_text_header_new_label (const gchar *label)
 {
diff --git a/composer/e-composer-text-header.h b/composer/e-composer-text-header.h
index 860fcc3..af97e7a 100644
--- a/composer/e-composer-text-header.h
+++ b/composer/e-composer-text-header.h
@@ -53,6 +53,8 @@ struct _EComposerTextHeader {
 
 struct _EComposerTextHeaderClass {
 	EComposerHeaderClass parent_class;
+
+	GType entry_type;
 };
 
 GType		e_composer_text_header_get_type	(void);
diff --git a/configure.ac b/configure.ac
index 2bde2e7..4d5cde0 100644
--- a/configure.ac
+++ b/configure.ac
@@ -38,7 +38,7 @@ dnl Required Packages
 m4_define([glib_minimum_version], [2.32.0])
 m4_define([gtk_minimum_version], [3.2.0])
 m4_define([eds_minimum_version], [evo_version])
-m4_define([gtkhtml_minimum_version], [4.1.2])
+m4_define([gtkhtml_minimum_version], [4.5.2])
 m4_define([gnome_desktop_minimum_version], [2.91.3])
 m4_define([gnome_icon_theme_minimum_version], [2.30.2.1])
 m4_define([gsettings_desktop_schemas_minimum_version], [2.91.92])
diff --git a/widgets/misc/Makefile.am b/widgets/misc/Makefile.am
index ab7efa0..5768c72 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 0000000..1f0e3c9
--- /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 0000000..2d6aaba
--- /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 */



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]