evolution-3.6.4/widgets/misc/e-spell-entry.c

No issues found

  1 /*
  2  * e-spell-entry.c
  3  *
  4  * This program is free software; you can redistribute it and/or
  5  * modify it under the terms of the GNU Lesser General Public
  6  * License as published by the Free Software Foundation; either
  7  * version 2 of the License, or (at your option) version 3.
  8  *
  9  * This program is distributed in the hope that it will be useful,
 10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 12  * Lesser General Public License for more details.
 13  *
 14  * You should have received a copy of the GNU Lesser General Public
 15  * License along with the program; if not, see <http://www.gnu.org/licenses/>
 16  *
 17  */
 18 
 19 /* This code is based on libsexy's SexySpellEntry */
 20 
 21 #ifdef HAVE_CONFIG_H
 22 #include <config.h>
 23 #endif
 24 
 25 #include <glib/gi18n-lib.h>
 26 #include <gtk/gtk.h>
 27 
 28 #include <editor/gtkhtml-spell-language.h>
 29 #include <editor/gtkhtml-spell-checker.h>
 30 
 31 #include "e-spell-entry.h"
 32 
 33 enum {
 34 	PROP_0,
 35 	PROP_CHECKING_ENABLED
 36 };
 37 
 38 struct _ESpellEntryPrivate
 39 {
 40 	PangoAttrList *attr_list;
 41 	gint mark_character;
 42 	gint entry_scroll_offset;
 43 	GSettings *settings;
 44 	gboolean custom_checkers;
 45 	gboolean checking_enabled;
 46 	GSList *checkers;
 47 	gchar **words;
 48 	gint *word_starts;
 49 	gint *word_ends;
 50 };
 51 
 52 #define E_SPELL_ENTRY_GET_PRIVATE(obj) \
 53 	(G_TYPE_INSTANCE_GET_PRIVATE \
 54 	((obj), E_TYPE_SPELL_ENTRY, ESpellEntryPrivate))
 55 
 56 G_DEFINE_TYPE (ESpellEntry, e_spell_entry, GTK_TYPE_ENTRY);
 57 
 58 static gboolean
 59 word_misspelled (ESpellEntry *entry,
 60                  gint start,
 61                  gint end)
 62 {
 63 	const gchar *text;
 64 	gchar *word;
 65 	gboolean result = TRUE;
 66 
 67 	if (start == end)
 68 		return FALSE;
 69 
 70 	text = gtk_entry_get_text (GTK_ENTRY (entry));
 71 	word = g_new0 (gchar, end - start + 2);
 72 
 73 	g_strlcpy (word, text + start, end - start + 1);
 74 
 75 	if (g_unichar_isalpha (*word)) {
 76 		GSList *li;
 77 		gssize wlen = strlen (word);
 78 
 79 		for (li = entry->priv->checkers; li; li = g_slist_next (li)) {
 80 			GtkhtmlSpellChecker *checker = li->data;
 81 			if (gtkhtml_spell_checker_check_word (checker, word, wlen)) {
 82 				result = FALSE;
 83 				break;
 84 			}
 85 		}
 86 	}
 87 	g_free (word);
 88 
 89 	return result;
 90 }
 91 
 92 static void
 93 insert_underline (ESpellEntry *entry,
 94                   guint start,
 95                   guint end)
 96 {
 97 	PangoAttribute *ucolor = pango_attr_underline_color_new (65535, 0, 0);
 98 	PangoAttribute *unline = pango_attr_underline_new (PANGO_UNDERLINE_ERROR);
 99 
100 	ucolor->start_index = start;
101 	unline->start_index = start;
102 
103 	ucolor->end_index = end;
104 	unline->end_index = end;
105 
106 	pango_attr_list_insert (entry->priv->attr_list, ucolor);
107 	pango_attr_list_insert (entry->priv->attr_list, unline);
108 }
109 
110 static void
111 check_word (ESpellEntry *entry,
112             gint start,
113             gint end)
114 {
115 	PangoAttrIterator *it;
116 
117 	/* Check to see if we've got any attributes at this position.
118 	 * If so, free them, since we'll readd it if the word is misspelled */
119 	it = pango_attr_list_get_iterator (entry->priv->attr_list);
120 
121 	if (it == NULL)
122 		return;
123 	do {
124 		gint s, e;
125 		pango_attr_iterator_range (it, &s, &e);
126 		if (s == start) {
127 			GSList *attrs = pango_attr_iterator_get_attrs (it);
128 			g_slist_foreach (attrs, (GFunc) pango_attribute_destroy, NULL);
129 			g_slist_free (attrs);
130 		}
131 	} while (pango_attr_iterator_next (it));
132 	pango_attr_iterator_destroy (it);
133 
134 	if (word_misspelled (entry, start, end))
135 		insert_underline (entry, start, end);
136 }
137 
138 static void
139 spell_entry_recheck_all (ESpellEntry *entry)
140 {
141 	GtkWidget *widget = GTK_WIDGET (entry);
142 	PangoLayout *layout;
143 	gint length, i;
144 
145 	if (!entry->priv->words)
146 		return;
147 
148 	/* Remove all existing pango attributes.  These will get read as we check */
149 	pango_attr_list_unref (entry->priv->attr_list);
150 	entry->priv->attr_list = pango_attr_list_new ();
151 
152 	if (entry->priv->checkers && entry->priv->checking_enabled) {
153 		/* Loop through words */
154 		for (i = 0; entry->priv->words[i]; i++) {
155 			length = strlen (entry->priv->words[i]);
156 			if (length == 0)
157 				continue;
158 			check_word (entry, entry->priv->word_starts[i], entry->priv->word_ends[i]);
159 		}
160 
161 		layout = gtk_entry_get_layout (GTK_ENTRY (entry));
162 		pango_layout_set_attributes (layout, entry->priv->attr_list);
163 	}
164 
165 	if (gtk_widget_get_realized (widget))
166 		gtk_widget_queue_draw (widget);
167 }
168 
169 static void
170 get_word_extents_from_position (ESpellEntry *entry,
171                                 gint *start,
172                                 gint *end,
173                                 guint position)
174 {
175 	const gchar *text;
176 	gint i, bytes_pos;
177 
178 	*start = -1;
179 	*end = -1;
180 
181 	if (entry->priv->words == NULL)
182 		return;
183 
184 	text = gtk_entry_get_text (GTK_ENTRY (entry));
185 	bytes_pos = (gint) (g_utf8_offset_to_pointer (text, position) - text);
186 
187 	for (i = 0; entry->priv->words[i]; i++) {
188 		if (bytes_pos >= entry->priv->word_starts[i] &&
189 		    bytes_pos <= entry->priv->word_ends[i]) {
190 			*start = entry->priv->word_starts[i];
191 			*end   = entry->priv->word_ends[i];
192 			return;
193 		}
194 	}
195 }
196 
197 static void
198 entry_strsplit_utf8 (GtkEntry *entry,
199                      gchar ***set,
200                      gint **starts,
201                      gint **ends)
202 {
203 	PangoLayout   *layout;
204 	PangoLogAttr  *log_attrs;
205 	const gchar   *text;
206 	gint           n_attrs, n_strings, i, j;
207 
208 	layout = gtk_entry_get_layout (GTK_ENTRY (entry));
209 	text = gtk_entry_get_text (GTK_ENTRY (entry));
210 	pango_layout_get_log_attrs (layout, &log_attrs, &n_attrs);
211 
212 	/* Find how many words we have */
213 	n_strings = 0;
214 	for (i = 0; i < n_attrs; i++)
215 		if (log_attrs[i].is_word_start)
216 			n_strings++;
217 
218 	*set    = g_new0 (gchar *, n_strings + 1);
219 	*starts = g_new0 (gint, n_strings);
220 	*ends   = g_new0 (gint, n_strings);
221 
222 	/* Copy out strings */
223 	for (i = 0, j = 0; i < n_attrs; i++) {
224 		if (log_attrs[i].is_word_start) {
225 			gint cend, bytes;
226 			gchar *start;
227 
228 			/* Find the end of this string */
229 			cend = i;
230 			while (!(log_attrs[cend].is_word_end))
231 				cend++;
232 
233 			/* Copy sub-string */
234 			start = g_utf8_offset_to_pointer (text, i);
235 			bytes = (gint) (g_utf8_offset_to_pointer (text, cend) - start);
236 			(*set)[j]    = g_new0 (gchar, bytes + 1);
237 			(*starts)[j] = (gint) (start - text);
238 			(*ends)[j]   = (gint) (start - text + bytes);
239 			g_utf8_strncpy ((*set)[j], start, cend - i);
240 
241 			/* Move on to the next word */
242 			j++;
243 		}
244 	}
245 
246 	g_free (log_attrs);
247 }
248 
249 static void
250 add_to_dictionary (GtkWidget *menuitem,
251                    ESpellEntry *entry)
252 {
253 	gchar *word;
254 	gint start, end;
255 	GtkhtmlSpellChecker *checker;
256 
257 	get_word_extents_from_position (entry, &start, &end, entry->priv->mark_character);
258 	word = gtk_editable_get_chars (GTK_EDITABLE (entry), start, end);
259 
260 	checker = g_object_get_data (G_OBJECT (menuitem), "spell-entry-checker");
261 	if (checker)
262 		gtkhtml_spell_checker_add_word (checker, word, -1);
263 
264 	g_free (word);
265 
266 	if (entry->priv->words) {
267 		g_strfreev (entry->priv->words);
268 		g_free (entry->priv->word_starts);
269 		g_free (entry->priv->word_ends);
270 	}
271 
272 	entry_strsplit_utf8 (GTK_ENTRY (entry), &entry->priv->words, &entry->priv->word_starts, &entry->priv->word_ends);
273 	spell_entry_recheck_all (entry);
274 }
275 
276 static void
277 ignore_all (GtkWidget *menuitem,
278             ESpellEntry *entry)
279 {
280 	gchar *word;
281 	gint start, end;
282 	GSList *li;
283 
284 	get_word_extents_from_position (entry, &start, &end, entry->priv->mark_character);
285 	word = gtk_editable_get_chars (GTK_EDITABLE (entry), start, end);
286 
287 	for (li = entry->priv->checkers; li; li = g_slist_next (li)) {
288 		GtkhtmlSpellChecker *checker = li->data;
289 		gtkhtml_spell_checker_add_word_to_session (checker, word, -1);
290 	}
291 
292 	g_free (word);
293 
294 	if (entry->priv->words) {
295 		g_strfreev (entry->priv->words);
296 		g_free (entry->priv->word_starts);
297 		g_free (entry->priv->word_ends);
298 	}
299 	entry_strsplit_utf8 (GTK_ENTRY (entry), &entry->priv->words, &entry->priv->word_starts, &entry->priv->word_ends);
300 	spell_entry_recheck_all (entry);
301 }
302 
303 static void
304 replace_word (GtkWidget *menuitem,
305               ESpellEntry *entry)
306 {
307 	gchar *oldword;
308 	const gchar *newword;
309 	gint start, end;
310 	gint cursor;
311 	GtkhtmlSpellChecker *checker;
312 
313 	get_word_extents_from_position (entry, &start, &end, entry->priv->mark_character);
314 	oldword = gtk_editable_get_chars (GTK_EDITABLE (entry), start, end);
315 	newword = gtk_label_get_text (GTK_LABEL (gtk_bin_get_child (GTK_BIN (menuitem))));
316 
317 	cursor = gtk_editable_get_position (GTK_EDITABLE (entry));
318 	/* is the cursor at the end? If so, restore it there */
319 	if (g_utf8_strlen (gtk_entry_get_text (GTK_ENTRY (entry)), -1) == cursor)
320 		cursor = -1;
321 	else if (cursor > start && cursor <= end)
322 		cursor = start;
323 
324 	gtk_editable_delete_text (GTK_EDITABLE (entry), start, end);
325 	gtk_editable_set_position (GTK_EDITABLE (entry), start);
326 	gtk_editable_insert_text (
327 		GTK_EDITABLE (entry), newword, strlen (newword),
328 		&start);
329 	gtk_editable_set_position (GTK_EDITABLE (entry), cursor);
330 
331 	checker = g_object_get_data (G_OBJECT (menuitem), "spell-entry-checker");
332 
333 	if (checker)
334 		gtkhtml_spell_checker_store_replacement (checker, oldword, -1, newword, -1);
335 
336 	g_free (oldword);
337 }
338 
339 static void
340 build_suggestion_menu (ESpellEntry *entry,
341                        GtkWidget *menu,
342                        GtkhtmlSpellChecker *checker,
343                        const gchar *word)
344 {
345 	GtkWidget *mi;
346 	GList *suggestions, *iter;
347 
348 	suggestions = gtkhtml_spell_checker_get_suggestions (checker, word, -1);
349 
350 	if (!suggestions) {
351 		/* no suggestions. Put something in the menu anyway... */
352 		GtkWidget *label = gtk_label_new (_("(no suggestions)"));
353 		PangoAttribute *attribute;
354 		PangoAttrList *attribute_list;
355 
356 		attribute_list = pango_attr_list_new ();
357 		attribute = pango_attr_style_new (PANGO_STYLE_ITALIC);
358 		pango_attr_list_insert (attribute_list, attribute);
359 		gtk_label_set_attributes (GTK_LABEL (label), attribute_list);
360 		pango_attr_list_unref (attribute_list);
361 
362 		mi = gtk_separator_menu_item_new ();
363 		gtk_container_add (GTK_CONTAINER (mi), label);
364 		gtk_widget_show_all (mi);
365 		gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), mi);
366 	} else {
367 		gint ii = 0;
368 
369 		/* build a set of menus with suggestions */
370 		for (iter = suggestions; iter; iter = g_list_next (iter), ii++) {
371 			if ((ii != 0) && (ii % 10 == 0)) {
372 				mi = gtk_separator_menu_item_new ();
373 				gtk_widget_show (mi);
374 				gtk_menu_shell_append (GTK_MENU_SHELL (menu), mi);
375 
376 				mi = gtk_menu_item_new_with_label (_("More..."));
377 				gtk_widget_show (mi);
378 				gtk_menu_shell_append (GTK_MENU_SHELL (menu), mi);
379 
380 				menu = gtk_menu_new ();
381 				gtk_menu_item_set_submenu (GTK_MENU_ITEM (mi), menu);
382 			}
383 
384 			mi = gtk_menu_item_new_with_label (iter->data);
385 			g_object_set_data (G_OBJECT (mi), "spell-entry-checker", checker);
386 			g_signal_connect (mi, "activate", G_CALLBACK (replace_word), entry);
387 			gtk_widget_show (mi);
388 			gtk_menu_shell_append (GTK_MENU_SHELL (menu), mi);
389 		}
390 	}
391 
392 	g_list_free_full (suggestions, g_free);
393 }
394 
395 static GtkWidget *
396 build_spelling_menu (ESpellEntry *entry,
397                      const gchar *word)
398 {
399 	GtkhtmlSpellChecker *checker;
400 	GtkWidget *topmenu, *mi;
401 	gchar *label;
402 
403 	topmenu = gtk_menu_new ();
404 
405 	if (!entry->priv->checkers)
406 		return topmenu;
407 
408 	/* Suggestions */
409 	if (!entry->priv->checkers->next) {
410 		checker = entry->priv->checkers->data;
411 		build_suggestion_menu (entry, topmenu, checker, word);
412 	} else {
413 		GSList *li;
414 		GtkWidget *menu;
415 		const gchar *lang_name;
416 
417 		for (li = entry->priv->checkers; li; li = g_slist_next (li)) {
418 			const GtkhtmlSpellLanguage *language;
419 
420 			checker = li->data;
421 			language = gtkhtml_spell_checker_get_language (checker);
422 			if (!language)
423 				continue;
424 
425 			lang_name = gtkhtml_spell_language_get_name (language);
426 			if (!lang_name)
427 				lang_name = gtkhtml_spell_language_get_code (language);
428 
429 			mi = gtk_menu_item_new_with_label (lang_name ? lang_name : "???");
430 
431 			gtk_widget_show (mi);
432 			gtk_menu_shell_append (GTK_MENU_SHELL (topmenu), mi);
433 			menu = gtk_menu_new ();
434 			gtk_menu_item_set_submenu (GTK_MENU_ITEM (mi), menu);
435 			build_suggestion_menu (entry, menu, checker, word);
436 		}
437 	}
438 
439 	/* Separator */
440 	mi = gtk_separator_menu_item_new ();
441 	gtk_widget_show (mi);
442 	gtk_menu_shell_append (GTK_MENU_SHELL (topmenu), mi);
443 
444 	/* + Add to Dictionary */
445 	label = g_strdup_printf (_("Add \"%s\" to Dictionary"), word);
446 	mi = gtk_image_menu_item_new_with_label (label);
447 	g_free (label);
448 
449 	gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (mi), gtk_image_new_from_stock (GTK_STOCK_ADD, GTK_ICON_SIZE_MENU));
450 
451 	if (!entry->priv->checkers->next) {
452 		checker = entry->priv->checkers->data;
453 		g_object_set_data (G_OBJECT (mi), "spell-entry-checker", checker);
454 		g_signal_connect (mi, "activate", G_CALLBACK (add_to_dictionary), entry);
455 	} else {
456 		GSList *li;
457 		GtkWidget *menu, *submi;
458 		const gchar *lang_name;
459 
460 		menu = gtk_menu_new ();
461 		gtk_menu_item_set_submenu (GTK_MENU_ITEM (mi), menu);
462 
463 		for (li = entry->priv->checkers; li; li = g_slist_next (li)) {
464 			const GtkhtmlSpellLanguage *language;
465 
466 			checker = li->data;
467 			language = gtkhtml_spell_checker_get_language (checker);
468 			if (!language)
469 				continue;
470 
471 			lang_name = gtkhtml_spell_language_get_name (language);
472 			if (!lang_name)
473 				lang_name = gtkhtml_spell_language_get_code (language);
474 
475 			submi = gtk_menu_item_new_with_label (lang_name ? lang_name : "???");
476 			g_object_set_data (G_OBJECT (submi), "spell-entry-checker", checker);
477 			g_signal_connect (submi, "activate", G_CALLBACK (add_to_dictionary), entry);
478 
479 			gtk_widget_show (submi);
480 			gtk_menu_shell_append (GTK_MENU_SHELL (menu), submi);
481 		}
482 	}
483 
484 	gtk_widget_show_all (mi);
485 	gtk_menu_shell_append (GTK_MENU_SHELL (topmenu), mi);
486 
487 	/* - Ignore All */
488 	mi = gtk_image_menu_item_new_with_label (_("Ignore All"));
489 	gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (mi), gtk_image_new_from_stock (GTK_STOCK_REMOVE, GTK_ICON_SIZE_MENU));
490 	g_signal_connect (mi, "activate", G_CALLBACK (ignore_all), entry);
491 	gtk_widget_show_all (mi);
492 	gtk_menu_shell_append (GTK_MENU_SHELL (topmenu), mi);
493 
494 	return topmenu;
495 }
496 
497 static void
498 spell_entry_add_suggestions_menu (ESpellEntry *entry,
499                                   GtkMenu *menu,
500                                   const gchar *word)
501 {
502 	GtkWidget *icon, *mi;
503 
504 	g_return_if_fail (menu != NULL);
505 	g_return_if_fail (word != NULL);
506 
507 	/* separator */
508 	mi = gtk_separator_menu_item_new ();
509 	gtk_widget_show (mi);
510 	gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), mi);
511 
512 	/* Above the separator, show the suggestions menu */
513 	icon = gtk_image_new_from_stock (GTK_STOCK_SPELL_CHECK, GTK_ICON_SIZE_MENU);
514 	mi = gtk_image_menu_item_new_with_label (_("Spelling Suggestions"));
515 	gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (mi), icon);
516 
517 	gtk_menu_item_set_submenu (GTK_MENU_ITEM (mi), build_spelling_menu (entry, word));
518 
519 	gtk_widget_show_all (mi);
520 	gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), mi);
521 }
522 
523 static gboolean
524 spell_entry_popup_menu (ESpellEntry *entry)
525 {
526 	/* Menu popped up from a keybinding (menu key or <shift>+F10). Use
527 	 * the cursor position as the mark position */
528 	entry->priv->mark_character = gtk_editable_get_position (GTK_EDITABLE (entry));
529 
530 	return FALSE;
531 }
532 
533 static void
534 spell_entry_populate_popup (ESpellEntry *entry,
535                             GtkMenu *menu,
536                             gpointer data)
537 {
538 	gint start, end;
539 	gchar *word;
540 
541 	if (!entry->priv->checkers)
542 		return;
543 
544 	get_word_extents_from_position (entry, &start, &end, entry->priv->mark_character);
545 	if (start == end)
546 		return;
547 
548 	if (!word_misspelled (entry, start, end))
549 		return;
550 
551 	word = gtk_editable_get_chars (GTK_EDITABLE (entry), start, end);
552 	g_return_if_fail (word != NULL);
553 
554 	spell_entry_add_suggestions_menu (entry, menu, word);
555 
556 	g_free (word);
557 }
558 
559 static void
560 spell_entry_changed (GtkEditable *editable)
561 {
562 	ESpellEntry *entry = E_SPELL_ENTRY (editable);
563 
564 	if (!entry->priv->checkers)
565 		return;
566 
567 	if (entry->priv->words) {
568 		g_strfreev (entry->priv->words);
569 		g_free (entry->priv->word_starts);
570 		g_free (entry->priv->word_ends);
571 	}
572 	entry_strsplit_utf8 (GTK_ENTRY (entry), &entry->priv->words, &entry->priv->word_starts, &entry->priv->word_ends);
573 	spell_entry_recheck_all (entry);
574 }
575 
576 static void
577 spell_entry_notify_scroll_offset (ESpellEntry *spell_entry)
578 {
579 	g_return_if_fail (spell_entry != NULL);
580 
581 	g_object_get (G_OBJECT (spell_entry), "scroll-offset", &spell_entry->priv->entry_scroll_offset, NULL);
582 }
583 
584 static GList *
585 spell_entry_load_spell_languages (void)
586 {
587 	GSettings *settings;
588 	GList *spell_languages = NULL;
589 	gchar **strv;
590 	gint ii;
591 
592 	/* Ask GSettings for a list of spell check language codes. */
593 	settings = g_settings_new ("org.gnome.evolution.mail");
594 	strv = g_settings_get_strv (settings, "composer-spell-languages");
595 	g_object_unref (settings);
596 
597 	/* Convert the codes to spell language structs. */
598 	for (ii = 0; strv[ii] != NULL; ii++) {
599 		gchar *language_code = strv[ii];
600 		const GtkhtmlSpellLanguage *language;
601 
602 		language = gtkhtml_spell_language_lookup (language_code);
603 		if (language != NULL)
604 			spell_languages = g_list_prepend (
605 				spell_languages, (gpointer) language);
606 	}
607 
608 	g_strfreev (strv);
609 
610 	spell_languages = g_list_reverse (spell_languages);
611 
612 	/* Pick a default spell language if it came back empty. */
613 	if (spell_languages == NULL) {
614 		const GtkhtmlSpellLanguage *language;
615 
616 		language = gtkhtml_spell_language_lookup (NULL);
617 
618 		if (language) {
619 			spell_languages = g_list_prepend (
620 				spell_languages, (gpointer) language);
621 		}
622 	}
623 
624 	return spell_languages;
625 }
626 
627 static void
628 spell_entry_settings_changed (ESpellEntry *spell_entry,
629                               const gchar *key)
630 {
631 	GList *languages;
632 
633 	g_return_if_fail (spell_entry != NULL);
634 
635 	if (spell_entry->priv->custom_checkers)
636 		return;
637 
638 	if (key && !g_str_equal (key, "composer-spell-languages"))
639 		return;
640 
641 	languages = spell_entry_load_spell_languages ();
642 	e_spell_entry_set_languages (spell_entry, languages);
643 	g_list_free (languages);
644 
645 	spell_entry->priv->custom_checkers = FALSE;
646 }
647 
648 static gint
649 spell_entry_find_position (ESpellEntry *spell_entry,
650                            gint x)
651 {
652 	PangoLayout *layout;
653 	PangoLayoutLine *line;
654 	gint index;
655 	gint pos;
656 	gint trailing;
657 	const gchar *text;
658 	GtkEntry *entry = GTK_ENTRY (spell_entry);
659 
660 	layout = gtk_entry_get_layout (entry);
661 	text = pango_layout_get_text (layout);
662 
663 	line = pango_layout_get_lines_readonly (layout)->data;
664 	pango_layout_line_x_to_index (line, x * PANGO_SCALE, &index, &trailing);
665 
666 	pos = g_utf8_pointer_to_offset (text, text + index);
667 	pos += trailing;
668 
669 	return pos;
670 }
671 
672 static gboolean
673 e_spell_entry_draw (GtkWidget *widget,
674                     cairo_t *cr)
675 {
676 	ESpellEntry *spell_entry = E_SPELL_ENTRY (widget);
677 	GtkEntry *entry = GTK_ENTRY (widget);
678 	PangoLayout *layout;
679 
680 	layout = gtk_entry_get_layout (entry);
681 	pango_layout_set_attributes (layout, spell_entry->priv->attr_list);
682 
683 	return GTK_WIDGET_CLASS (e_spell_entry_parent_class)->draw (widget, cr);
684 }
685 
686 static gboolean
687 e_spell_entry_button_press (GtkWidget *widget,
688                             GdkEventButton *event)
689 {
690 	ESpellEntry *spell_entry = E_SPELL_ENTRY (widget);
691 
692 	spell_entry->priv->mark_character = spell_entry_find_position (
693 		spell_entry, event->x + spell_entry->priv->entry_scroll_offset);
694 
695 	return GTK_WIDGET_CLASS (e_spell_entry_parent_class)->button_press_event (widget, event);
696 }
697 
698 static void
699 spell_entry_set_property (GObject *object,
700                           guint property_id,
701                           const GValue *value,
702                           GParamSpec *pspec)
703 {
704 	switch (property_id) {
705 		case PROP_CHECKING_ENABLED:
706 			e_spell_entry_set_checking_enabled (
707 				E_SPELL_ENTRY (object),
708 				g_value_get_boolean (value));
709 			return;
710 	}
711 
712 	G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
713 }
714 
715 static void
716 spell_entry_get_property (GObject *object,
717                                   guint property_id,
718                                   GValue *value,
719                                   GParamSpec *pspec)
720 {
721 	switch (property_id) {
722 		case PROP_CHECKING_ENABLED:
723 			g_value_set_boolean (
724 				value,
725 				e_spell_entry_get_checking_enabled (
726 				E_SPELL_ENTRY (object)));
727 			return;
728 	}
729 
730 	G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
731 }
732 
733 static void
734 e_spell_entry_init (ESpellEntry *spell_entry)
735 {
736 	spell_entry->priv = E_SPELL_ENTRY_GET_PRIVATE (spell_entry);
737 	spell_entry->priv->attr_list = pango_attr_list_new ();
738 	spell_entry->priv->checkers = NULL;
739 	spell_entry->priv->checking_enabled = TRUE;
740 
741 	g_signal_connect (spell_entry, "popup-menu", G_CALLBACK (spell_entry_popup_menu), NULL);
742 	g_signal_connect (spell_entry, "populate-popup", G_CALLBACK (spell_entry_populate_popup), NULL);
743 	g_signal_connect (spell_entry, "changed", G_CALLBACK (spell_entry_changed), NULL);
744 	g_signal_connect (spell_entry, "notify::scroll-offset", G_CALLBACK (spell_entry_notify_scroll_offset), NULL);
745 
746 	/* listen for languages changes */
747 	spell_entry->priv->settings = g_settings_new ("org.gnome.evolution.mail");
748 	g_signal_connect_swapped (spell_entry->priv->settings, "changed", G_CALLBACK (spell_entry_settings_changed), spell_entry);
749 
750 	/* load current settings */
751 	spell_entry_settings_changed (spell_entry, NULL);
752 }
753 
754 static void
755 e_spell_entry_finalize (GObject *object)
756 {
757 	ESpellEntry *entry;
758 
759 	g_return_if_fail (object != NULL);
760 	g_return_if_fail (E_IS_SPELL_ENTRY (object));
761 
762 	entry = E_SPELL_ENTRY (object);
763 
764 	if (entry->priv->settings)
765 		g_object_unref (entry->priv->settings);
766 	if (entry->priv->checkers)
767 		g_slist_free_full (entry->priv->checkers, g_object_unref);
768 	if (entry->priv->attr_list)
769 		pango_attr_list_unref (entry->priv->attr_list);
770 	if (entry->priv->words)
771 		g_strfreev (entry->priv->words);
772 	if (entry->priv->word_starts)
773 		g_free (entry->priv->word_starts);
774 	if (entry->priv->word_ends)
775 		g_free (entry->priv->word_ends);
776 
777 	G_OBJECT_CLASS (e_spell_entry_parent_class)->finalize (object);
778 }
779 
780 static void
781 e_spell_entry_class_init (ESpellEntryClass *class)
782 {
783 	GObjectClass *object_class;
784 	GtkWidgetClass *widget_class;
785 
786 	g_type_class_add_private (class, sizeof (ESpellEntryPrivate));
787 
788 	object_class = G_OBJECT_CLASS (class);
789 	object_class->set_property = spell_entry_set_property;
790 	object_class->get_property = spell_entry_get_property;
791 	object_class->finalize = e_spell_entry_finalize;
792 
793 	widget_class  = GTK_WIDGET_CLASS (class);
794 	widget_class->draw = e_spell_entry_draw;
795 	widget_class->button_press_event = e_spell_entry_button_press;
796 
797 	g_object_class_install_property (
798 		object_class,
799 		PROP_CHECKING_ENABLED,
800 		g_param_spec_boolean (
801 			"checking-enabled",
802 			"checking enabled",
803 			"Spell Checking is Enabled",
804 			TRUE,
805 			G_PARAM_READWRITE));
806 }
807 
808 GtkWidget *
809 e_spell_entry_new (void)
810 {
811 	return g_object_new (E_TYPE_SPELL_ENTRY, NULL);
812 }
813 
814 /* 'languages' consists of 'const GtkhtmlSpellLanguage *' */
815 void
816 e_spell_entry_set_languages (ESpellEntry *spell_entry,
817                              GList *languages)
818 {
819 	GList *iter;
820 
821 	g_return_if_fail (spell_entry != NULL);
822 
823 	spell_entry->priv->custom_checkers = TRUE;
824 
825 	if (spell_entry->priv->checkers)
826 		g_slist_free_full (spell_entry->priv->checkers, g_object_unref);
827 	spell_entry->priv->checkers = NULL;
828 
829 	for (iter = languages; iter; iter = g_list_next (iter)) {
830 		const GtkhtmlSpellLanguage *language = iter->data;
831 
832 		if (language)
833 			spell_entry->priv->checkers = g_slist_prepend (
834 				spell_entry->priv->checkers,
835 				gtkhtml_spell_checker_new (language));
836 	}
837 
838 	spell_entry->priv->checkers = g_slist_reverse (spell_entry->priv->checkers);
839 
840 	if (gtk_widget_get_realized (GTK_WIDGET (spell_entry)))
841 		spell_entry_recheck_all (spell_entry);
842 }
843 
844 gboolean
845 e_spell_entry_get_checking_enabled (ESpellEntry *spell_entry)
846 {
847 	g_return_val_if_fail (spell_entry != NULL, FALSE);
848 
849 	return spell_entry->priv->checking_enabled;
850 }
851 
852 void
853 e_spell_entry_set_checking_enabled (ESpellEntry *spell_entry,
854                                     gboolean enable_checking)
855 {
856 	g_return_if_fail (spell_entry != NULL);
857 
858 	if (spell_entry->priv->checking_enabled == enable_checking)
859 		return;
860 
861 	spell_entry->priv->checking_enabled = enable_checking;
862 	spell_entry_recheck_all (spell_entry);
863 
864 	g_object_notify (G_OBJECT (spell_entry), "checking-enabled");
865 
866 }