1 /*
2  * e-spell-entry.c
3  *
4  * This program is free software; you can redistribute it and/or modify it
5  * under the terms of the GNU Lesser General Public License as published by
6  * the Free Software Foundation.
7  *
8  * This program is distributed in the hope that it will be useful, but
9  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
10  * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
11  * for more details.
12  *
13  * You should have received a copy of the GNU Lesser General Public License
14  * along with this program; if not, see <http://www.gnu.org/licenses/>.
15  *
16  */
17 
18 /* This code is based on libsexy's SexySpellEntry */
19 
20 #include "evolution-config.h"
21 
22 #include <gtk/gtk.h>
23 #include <glib/gi18n-lib.h>
24 
25 #include <libebackend/libebackend.h>
26 
27 #include <e-util/e-spell-checker.h>
28 
29 #include "e-misc-utils.h"
30 #include "e-spell-entry.h"
31 
32 #define E_SPELL_ENTRY_GET_PRIVATE(obj) \
33 	(G_TYPE_INSTANCE_GET_PRIVATE \
34 	((obj), E_TYPE_SPELL_ENTRY, ESpellEntryPrivate))
35 
36 struct _ESpellEntryPrivate {
37 	PangoAttrList *attr_list;
38 	gint mark_character;
39 	gint entry_scroll_offset;
40 	gboolean custom_checkers;
41 	gboolean checking_enabled;
42 	gchar **words;
43 	gint *word_starts;
44 	gint *word_ends;
45 
46 	ESpellChecker *spell_checker;
47 	guint active_languages_handler_id;
48 
49 	gboolean im_in_preedit;
50 };
51 
52 enum {
53 	PROP_0,
54 	PROP_CHECKING_ENABLED,
55 	PROP_SPELL_CHECKER
56 };
57 
G_DEFINE_TYPE_WITH_CODE(ESpellEntry,e_spell_entry,GTK_TYPE_ENTRY,G_IMPLEMENT_INTERFACE (E_TYPE_EXTENSIBLE,NULL))58 G_DEFINE_TYPE_WITH_CODE (
59 	ESpellEntry,
60 	e_spell_entry,
61 	GTK_TYPE_ENTRY,
62 	G_IMPLEMENT_INTERFACE (
63 		E_TYPE_EXTENSIBLE, NULL))
64 
65 static gboolean
66 word_misspelled (ESpellEntry *entry,
67                  gint start,
68                  gint end)
69 {
70 	const gchar *text;
71 	gchar *word;
72 	ESpellChecker *spell_checker;
73 	gboolean result;
74 
75 	if (start == end)
76 		return FALSE;
77 
78 	text = gtk_entry_get_text (GTK_ENTRY (entry));
79 	word = g_new0 (gchar, end - start + 2);
80 
81 	g_strlcpy (word, text + start, end - start + 1);
82 
83 	spell_checker = e_spell_entry_get_spell_checker (entry);
84 	result = !e_spell_checker_check_word (spell_checker, word, -1);
85 
86 	g_free (word);
87 
88 	return result;
89 }
90 
91 static void
insert_underline(ESpellEntry * entry,guint start,guint end)92 insert_underline (ESpellEntry *entry,
93                   guint start,
94                   guint end)
95 {
96 	PangoAttribute *ucolor;
97 	PangoAttribute *unline;
98 
99 	ucolor = pango_attr_underline_color_new (65535, 0, 0);
100 	unline = pango_attr_underline_new (PANGO_UNDERLINE_ERROR);
101 
102 	ucolor->start_index = start;
103 	unline->start_index = start;
104 
105 	ucolor->end_index = end;
106 	unline->end_index = end;
107 
108 	pango_attr_list_insert (entry->priv->attr_list, ucolor);
109 	pango_attr_list_insert (entry->priv->attr_list, unline);
110 }
111 
112 static void
check_word(ESpellEntry * entry,gint start,gint end)113 check_word (ESpellEntry *entry,
114             gint start,
115             gint end)
116 {
117 	PangoAttrIterator *it;
118 
119 	/* Check to see if we've got any attributes at this position.
120 	 * If so, free them, since we'll read it if the word is misspelled */
121 	it = pango_attr_list_get_iterator (entry->priv->attr_list);
122 
123 	if (it == NULL)
124 		return;
125 	do {
126 		gint s, e;
127 		pango_attr_iterator_range (it, &s, &e);
128 		if (s == start) {
129 			/* XXX What does this do? */
130 			GSList *attrs = pango_attr_iterator_get_attrs (it);
131 			g_slist_free_full (
132 				attrs, (GDestroyNotify)
133 				pango_attribute_destroy);
134 		}
135 	} while (pango_attr_iterator_next (it));
136 	pango_attr_iterator_destroy (it);
137 
138 	if (word_misspelled (entry, start, end))
139 		insert_underline (entry, start, end);
140 }
141 
142 static void
spell_entry_recheck_all(ESpellEntry * entry)143 spell_entry_recheck_all (ESpellEntry *entry)
144 {
145 	GtkWidget *widget = GTK_WIDGET (entry);
146 	PangoLayout *layout;
147 	gint length, i;
148 	gboolean check_words = FALSE;
149 
150 	if (entry->priv->words == NULL)
151 		return;
152 
153 	/* Remove all existing pango attributes.
154 	 * These will get read as we check. */
155 	pango_attr_list_unref (entry->priv->attr_list);
156 	entry->priv->attr_list = pango_attr_list_new ();
157 
158 	if (e_spell_entry_get_checking_enabled (entry)) {
159 		ESpellChecker *spell_checker;
160 
161 		spell_checker = e_spell_entry_get_spell_checker (entry);
162 		if (e_spell_checker_count_active_languages (spell_checker) > 0)
163 			check_words = TRUE;
164 	}
165 
166 	if (check_words) {
167 		/* Loop through words */
168 		for (i = 0; entry->priv->words[i]; i++) {
169 			length = strlen (entry->priv->words[i]);
170 			if (length == 0)
171 				continue;
172 			check_word (
173 				entry,
174 				entry->priv->word_starts[i],
175 				entry->priv->word_ends[i]);
176 		}
177 
178 		layout = gtk_entry_get_layout (GTK_ENTRY (entry));
179 		pango_layout_set_attributes (layout, entry->priv->attr_list);
180 	}
181 
182 	if (gtk_widget_get_realized (widget))
183 		gtk_widget_queue_draw (widget);
184 }
185 
186 static void
get_word_extents_from_position(ESpellEntry * entry,gint * start,gint * end,guint position)187 get_word_extents_from_position (ESpellEntry *entry,
188                                 gint *start,
189                                 gint *end,
190                                 guint position)
191 {
192 	const gchar *text;
193 	gint i, bytes_pos;
194 
195 	*start = -1;
196 	*end = -1;
197 
198 	if (entry->priv->words == NULL)
199 		return;
200 
201 	text = gtk_entry_get_text (GTK_ENTRY (entry));
202 	bytes_pos = (gint) (g_utf8_offset_to_pointer (text, position) - text);
203 
204 	for (i = 0; entry->priv->words[i]; i++) {
205 		if (bytes_pos >= entry->priv->word_starts[i] &&
206 		    bytes_pos <= entry->priv->word_ends[i]) {
207 			*start = entry->priv->word_starts[i];
208 			*end = entry->priv->word_ends[i];
209 			return;
210 		}
211 	}
212 }
213 
214 static void
spell_entry_store_word(gchar *** set,gint ** starts,gint ** ends,const gchar * text,gint n_word,gint n_strings,const gchar * word_start,const gchar * word_end)215 spell_entry_store_word (gchar ***set,
216 			gint **starts,
217 			gint **ends,
218 			const gchar *text,
219 			gint n_word,
220 			gint n_strings,
221 			const gchar *word_start,
222 			const gchar *word_end)
223 {
224 	gint bytes;
225 
226 	g_return_if_fail (n_word >= 0);
227 	g_return_if_fail (n_word < n_strings);
228 
229 	/* Copy sub-string */
230 	bytes = (gint) (word_end - word_start);
231 	(*set)[n_word] = g_new0 (gchar, bytes + 1);
232 	(*starts)[n_word] = (gint) (word_start - text);
233 	(*ends)[n_word] = (gint) (word_start - text + bytes);
234 	memcpy ((*set)[n_word], word_start, bytes);
235 }
236 
237 static gboolean
entry_is_word_char(gunichar uc,gboolean has_en_language)238 entry_is_word_char (gunichar uc,
239 		    gboolean has_en_language)
240 {
241 	return (uc == L'\'' && has_en_language) ||
242 		g_unichar_isalnum (uc) ||
243 		g_unichar_ismark (uc);
244 }
245 
246 static void
entry_strsplit_utf8(ESpellEntry * entry,gchar *** set,gint ** starts,gint ** ends)247 entry_strsplit_utf8 (ESpellEntry *entry,
248                      gchar ***set,
249                      gint **starts,
250                      gint **ends)
251 {
252 	const gchar *text, *ptr, *word_start;
253 	gint n_strings, n_word;
254 	gchar **active_languages;
255 	guint n_languages, ii;
256 	gboolean has_en_language = FALSE;
257 
258 	text = gtk_entry_get_text (GTK_ENTRY (entry));
259 	g_return_if_fail (g_utf8_validate (text, -1, NULL));
260 
261 	active_languages = e_spell_checker_list_active_languages (entry->priv->spell_checker, &n_languages);
262 	for (ii = 0; active_languages && ii < n_languages && !has_en_language; ii++) {
263 		has_en_language =
264 			g_ascii_strncasecmp (active_languages[ii], "en", 2) == 0 &&
265 			(!active_languages[ii][2] || active_languages[ii][2] == '_');
266 	}
267 
268 	g_strfreev (active_languages);
269 
270 	/* Find how many words we have */
271 	n_strings = 0;
272 	word_start = NULL;
273 	for (ptr = text; *ptr; ptr = g_utf8_next_char (ptr)) {
274 		if (!entry_is_word_char (g_utf8_get_char (ptr), has_en_language)) {
275 			word_start = NULL;
276 		} else if (!word_start) {
277 			n_strings++;
278 			word_start = ptr;
279 		}
280 	}
281 
282 	*set = g_new0 (gchar *, n_strings + 1);
283 	*starts = g_new0 (gint, n_strings + 1);
284 	*ends = g_new0 (gint, n_strings + 1);
285 
286 	/* Copy out strings */
287 	word_start = NULL;
288 	n_word = -1;
289 	for (ptr = text; *ptr; ptr = g_utf8_next_char (ptr)) {
290 		if (!entry_is_word_char (g_utf8_get_char (ptr), has_en_language)) {
291 			if (word_start)
292 				spell_entry_store_word (set, starts, ends, text, n_word, n_strings, word_start, ptr);
293 			word_start = NULL;
294 		} else if (!word_start) {
295 			n_word++;
296 			word_start = ptr;
297 		}
298 	}
299 
300 	if (word_start)
301 		spell_entry_store_word (set, starts, ends, text, n_word, n_strings, word_start, ptr);
302 }
303 
304 static gchar *
spell_entry_get_chars_from_byte_pos(ESpellEntry * entry,gint byte_pos_start,gint byte_pos_end)305 spell_entry_get_chars_from_byte_pos (ESpellEntry *entry,
306 				     gint byte_pos_start,
307 				     gint byte_pos_end)
308 {
309 	const gchar *text;
310 	gint len;
311 
312 	g_return_val_if_fail (E_IS_SPELL_ENTRY (entry), NULL);
313 	g_return_val_if_fail (byte_pos_start <= byte_pos_end, NULL);
314 
315 	text = gtk_entry_get_text (GTK_ENTRY (entry));
316 	if (!text)
317 		return NULL;
318 
319 	len = strlen (text);
320 
321 	if (byte_pos_start < 0)
322 		byte_pos_start = 0;
323 
324 	if (byte_pos_end > len)
325 		byte_pos_end = len;
326 
327 	if (byte_pos_end < 0)
328 		byte_pos_end = 0;
329 
330 	return g_strndup (text + byte_pos_start, byte_pos_end - byte_pos_start);
331 }
332 
333 static void
spell_entry_byte_pos_to_char_pos(ESpellEntry * entry,gint byte_pos,gint * out_char_pos)334 spell_entry_byte_pos_to_char_pos (ESpellEntry *entry,
335 				  gint byte_pos,
336 				  gint *out_char_pos)
337 {
338 	const gchar *text, *ptr;
339 
340 	g_return_if_fail (E_IS_SPELL_ENTRY (entry));
341 	g_return_if_fail (out_char_pos != NULL);
342 
343 	*out_char_pos = 0;
344 
345 	if (byte_pos <= 0)
346 		return;
347 
348 	text = gtk_entry_get_text (GTK_ENTRY (entry));
349 	if (!text || !g_utf8_validate (text, -1, NULL))
350 		return;
351 
352 	for (ptr = text; ptr && *ptr; ptr = g_utf8_next_char (ptr)) {
353 		if (byte_pos <= ptr - text)
354 			break;
355 
356 		*out_char_pos = (*out_char_pos) + 1;
357 	}
358 }
359 
360 static void
add_to_dictionary(GtkWidget * menuitem,ESpellEntry * entry)361 add_to_dictionary (GtkWidget *menuitem,
362                    ESpellEntry *entry)
363 {
364 	gchar *word;
365 	gint start, end;
366 	ESpellDictionary *dict;
367 
368 	get_word_extents_from_position (
369 		entry, &start, &end, entry->priv->mark_character);
370 	word = spell_entry_get_chars_from_byte_pos (entry, start, end);
371 
372 	dict = g_object_get_data (G_OBJECT (menuitem), "spell-entry-checker");
373 	if (dict != NULL)
374 		e_spell_dictionary_learn_word (dict, word, -1);
375 
376 	g_free (word);
377 
378 	if (entry->priv->words != NULL) {
379 		g_strfreev (entry->priv->words);
380 		g_free (entry->priv->word_starts);
381 		g_free (entry->priv->word_ends);
382 	}
383 
384 	entry_strsplit_utf8 (
385 		entry,
386 		&entry->priv->words,
387 		&entry->priv->word_starts,
388 		&entry->priv->word_ends);
389 
390 	spell_entry_recheck_all (entry);
391 }
392 
393 static void
ignore_all(GtkWidget * menuitem,ESpellEntry * entry)394 ignore_all (GtkWidget *menuitem,
395             ESpellEntry *entry)
396 {
397 	ESpellChecker *spell_checker;
398 	gchar *word;
399 	gint start, end;
400 
401 	get_word_extents_from_position (
402 		entry, &start, &end, entry->priv->mark_character);
403 	word = spell_entry_get_chars_from_byte_pos (entry, start, end);
404 
405 	spell_checker = e_spell_entry_get_spell_checker (entry);
406 	e_spell_checker_ignore_word (spell_checker, word);
407 
408 	g_free (word);
409 
410 	if (entry->priv->words != NULL) {
411 		g_strfreev (entry->priv->words);
412 		g_free (entry->priv->word_starts);
413 		g_free (entry->priv->word_ends);
414 	}
415 
416 	entry_strsplit_utf8 (
417 		entry,
418 		&entry->priv->words,
419 		&entry->priv->word_starts,
420 		&entry->priv->word_ends);
421 
422 	spell_entry_recheck_all (entry);
423 }
424 
425 static void
replace_word(GtkWidget * menuitem,ESpellEntry * entry)426 replace_word (GtkWidget *menuitem,
427               ESpellEntry *entry)
428 {
429 	gchar *oldword;
430 	const gchar *newword;
431 	gint start, end;
432 	gint cursor;
433 	ESpellDictionary *dict;
434 
435 	get_word_extents_from_position (
436 		entry, &start, &end, entry->priv->mark_character);
437 	oldword = spell_entry_get_chars_from_byte_pos (entry, start, end);
438 	newword = gtk_label_get_text (
439 		GTK_LABEL (gtk_bin_get_child (GTK_BIN (menuitem))));
440 
441 	spell_entry_byte_pos_to_char_pos (entry, start, &start);
442 	spell_entry_byte_pos_to_char_pos (entry, end, &end);
443 
444 	cursor = gtk_editable_get_position (GTK_EDITABLE (entry));
445 	/* is the cursor at the end? If so, restore it there */
446 	if (g_utf8_strlen (gtk_entry_get_text (GTK_ENTRY (entry)), -1) == cursor)
447 		cursor = -1;
448 	else if (cursor > start && cursor <= end)
449 		cursor = start;
450 
451 	gtk_editable_delete_text (GTK_EDITABLE (entry), start, end);
452 	gtk_editable_set_position (GTK_EDITABLE (entry), start);
453 	gtk_editable_insert_text (
454 		GTK_EDITABLE (entry), newword, strlen (newword), &start);
455 	gtk_editable_set_position (GTK_EDITABLE (entry), cursor);
456 
457 	dict = g_object_get_data (G_OBJECT (menuitem), "spell-entry-checker");
458 
459 	if (dict != NULL)
460 		e_spell_dictionary_store_correction (
461 			dict, oldword, -1, newword, -1);
462 
463 	g_free (oldword);
464 }
465 
466 static void
build_suggestion_menu(ESpellEntry * entry,GtkWidget * menu,ESpellDictionary * dict,const gchar * word)467 build_suggestion_menu (ESpellEntry *entry,
468                        GtkWidget *menu,
469                        ESpellDictionary *dict,
470                        const gchar *word)
471 {
472 	GtkWidget *mi;
473 	GList *suggestions, *iter;
474 
475 	suggestions = e_spell_dictionary_get_suggestions (dict, word, -1);
476 
477 	if (suggestions == NULL) {
478 		/* no suggestions. Put something in the menu anyway... */
479 		GtkWidget *label = gtk_label_new (_("(no suggestions)"));
480 		PangoAttribute *attribute;
481 		PangoAttrList *attribute_list;
482 
483 		attribute_list = pango_attr_list_new ();
484 		attribute = pango_attr_style_new (PANGO_STYLE_ITALIC);
485 		pango_attr_list_insert (attribute_list, attribute);
486 		gtk_label_set_attributes (GTK_LABEL (label), attribute_list);
487 		pango_attr_list_unref (attribute_list);
488 
489 		mi = gtk_separator_menu_item_new ();
490 		gtk_container_add (GTK_CONTAINER (mi), label);
491 		gtk_widget_show_all (mi);
492 		gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), mi);
493 	} else {
494 		gint ii = 0;
495 
496 		/* build a set of menus with suggestions */
497 		for (iter = suggestions; iter; iter = g_list_next (iter), ii++) {
498 			if ((ii != 0) && (ii % 10 == 0)) {
499 				mi = gtk_separator_menu_item_new ();
500 				gtk_widget_show (mi);
501 				gtk_menu_shell_append (GTK_MENU_SHELL (menu), mi);
502 
503 				mi = gtk_menu_item_new_with_label (_("More…"));
504 				gtk_widget_show (mi);
505 				gtk_menu_shell_append (GTK_MENU_SHELL (menu), mi);
506 
507 				menu = gtk_menu_new ();
508 				gtk_menu_item_set_submenu (GTK_MENU_ITEM (mi), menu);
509 			}
510 
511 			mi = gtk_menu_item_new_with_label (iter->data);
512 			g_object_set_data (G_OBJECT (mi), "spell-entry-checker", dict);
513 			g_signal_connect (mi, "activate", G_CALLBACK (replace_word), entry);
514 			gtk_widget_show (mi);
515 			gtk_menu_shell_append (GTK_MENU_SHELL (menu), mi);
516 		}
517 	}
518 
519 	g_list_free_full (suggestions, (GDestroyNotify) g_free);
520 }
521 
522 static GtkWidget *
build_spelling_menu(ESpellEntry * entry,const gchar * word)523 build_spelling_menu (ESpellEntry *entry,
524                      const gchar *word)
525 {
526 	ESpellChecker *spell_checker;
527 	ESpellDictionary *dict;
528 	GtkWidget *topmenu, *mi;
529 	GQueue queue = G_QUEUE_INIT;
530 	gchar **active_languages;
531 	guint ii, n_active_languages;
532 	gchar *label;
533 
534 	topmenu = gtk_menu_new ();
535 
536 	spell_checker = e_spell_entry_get_spell_checker (entry);
537 
538 	active_languages = e_spell_checker_list_active_languages (
539 		spell_checker, &n_active_languages);
540 	for (ii = 0; ii < n_active_languages; ii++) {
541 		dict = e_spell_checker_ref_dictionary (
542 			spell_checker, active_languages[ii]);
543 		if (dict != NULL)
544 			g_queue_push_tail (&queue, dict);
545 	}
546 	g_strfreev (active_languages);
547 
548 	if (g_queue_is_empty (&queue))
549 		goto exit;
550 
551 	/* Suggestions */
552 	if (n_active_languages == 1) {
553 		dict = g_queue_peek_head (&queue);
554 		build_suggestion_menu (entry, topmenu, dict, word);
555 	} else {
556 		GtkWidget *menu;
557 		GList *list, *link;
558 
559 		list = g_queue_peek_head_link (&queue);
560 
561 		for (link = list; link != NULL; link = g_list_next (link)) {
562 			const gchar *lang_name;
563 
564 			dict = E_SPELL_DICTIONARY (link->data);
565 
566 			lang_name = e_spell_dictionary_get_name (dict);
567 			if (lang_name == NULL)
568 				lang_name = e_spell_dictionary_get_code (dict);
569 			if (lang_name == NULL)
570 				lang_name = "???";
571 
572 			mi = gtk_menu_item_new_with_label (lang_name);
573 
574 			gtk_widget_show (mi);
575 			gtk_menu_shell_append (GTK_MENU_SHELL (topmenu), mi);
576 			menu = gtk_menu_new ();
577 			gtk_menu_item_set_submenu (GTK_MENU_ITEM (mi), menu);
578 			build_suggestion_menu (entry, menu, dict, word);
579 		}
580 	}
581 
582 	/* Separator */
583 	mi = gtk_separator_menu_item_new ();
584 	gtk_widget_show (mi);
585 	gtk_menu_shell_append (GTK_MENU_SHELL (topmenu), mi);
586 
587 	/* + Add to Dictionary */
588 	label = g_strdup_printf (_("Add “%s” to Dictionary"), word);
589 	mi = gtk_image_menu_item_new_with_label (label);
590 	g_free (label);
591 
592 	gtk_image_menu_item_set_image (
593 		GTK_IMAGE_MENU_ITEM (mi),
594 		gtk_image_new_from_icon_name ("list-add", GTK_ICON_SIZE_MENU));
595 
596 	if (n_active_languages == 1) {
597 		dict = g_queue_peek_head (&queue);
598 		g_object_set_data (G_OBJECT (mi), "spell-entry-checker", dict);
599 		g_signal_connect (
600 			mi, "activate",
601 			G_CALLBACK (add_to_dictionary), entry);
602 	} else {
603 		GtkWidget *menu, *submi;
604 		GList *list, *link;
605 
606 		menu = gtk_menu_new ();
607 		gtk_menu_item_set_submenu (GTK_MENU_ITEM (mi), menu);
608 
609 		list = g_queue_peek_head_link (&queue);
610 
611 		for (link = list; link != NULL; link = g_list_next (link)) {
612 			const gchar *lang_name;
613 
614 			dict = E_SPELL_DICTIONARY (link->data);
615 
616 			lang_name = e_spell_dictionary_get_name (dict);
617 			if (lang_name == NULL)
618 				lang_name = e_spell_dictionary_get_code (dict);
619 			if (lang_name == NULL)
620 				lang_name = "???";
621 
622 			submi = gtk_menu_item_new_with_label (lang_name);
623 			g_object_set_data (G_OBJECT (submi), "spell-entry-checker", dict);
624 			g_signal_connect (
625 				submi, "activate",
626 				G_CALLBACK (add_to_dictionary), entry);
627 
628 			gtk_widget_show (submi);
629 			gtk_menu_shell_append (GTK_MENU_SHELL (menu), submi);
630 		}
631 	}
632 
633 	gtk_widget_show_all (mi);
634 	gtk_menu_shell_append (GTK_MENU_SHELL (topmenu), mi);
635 
636 	/* - Ignore All */
637 	mi = gtk_image_menu_item_new_with_label (_("Ignore All"));
638 	gtk_image_menu_item_set_image (
639 		GTK_IMAGE_MENU_ITEM (mi),
640 		gtk_image_new_from_icon_name ("list-remove", GTK_ICON_SIZE_MENU));
641 	g_signal_connect (mi, "activate", G_CALLBACK (ignore_all), entry);
642 	gtk_widget_show_all (mi);
643 	gtk_menu_shell_append (GTK_MENU_SHELL (topmenu), mi);
644 
645 exit:
646 	while (!g_queue_is_empty (&queue))
647 		g_object_unref (g_queue_pop_head (&queue));
648 
649 	return topmenu;
650 }
651 
652 static void
spell_entry_add_suggestions_menu(ESpellEntry * entry,GtkMenu * menu,const gchar * word)653 spell_entry_add_suggestions_menu (ESpellEntry *entry,
654                                   GtkMenu *menu,
655                                   const gchar *word)
656 {
657 	GtkWidget *icon, *mi;
658 
659 	g_return_if_fail (menu != NULL);
660 	g_return_if_fail (word != NULL);
661 
662 	/* separator */
663 	mi = gtk_separator_menu_item_new ();
664 	gtk_widget_show (mi);
665 	gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), mi);
666 
667 	/* Above the separator, show the suggestions menu */
668 	icon = gtk_image_new_from_icon_name ("tools-check-spelling", GTK_ICON_SIZE_MENU);
669 	mi = gtk_image_menu_item_new_with_label (_("Spelling Suggestions"));
670 	gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (mi), icon);
671 
672 	gtk_menu_item_set_submenu (GTK_MENU_ITEM (mi), build_spelling_menu (entry, word));
673 
674 	gtk_widget_show_all (mi);
675 	gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), mi);
676 }
677 
678 static gboolean
spell_entry_popup_menu(ESpellEntry * entry)679 spell_entry_popup_menu (ESpellEntry *entry)
680 {
681 	/* Menu popped up from a keybinding (menu key or <shift>+F10).
682 	 * Use the cursor position as the mark position. */
683 	entry->priv->mark_character =
684 		gtk_editable_get_position (GTK_EDITABLE (entry));
685 
686 	return FALSE;
687 }
688 
689 static void
spell_entry_populate_popup(ESpellEntry * entry,GtkMenu * menu,gpointer data)690 spell_entry_populate_popup (ESpellEntry *entry,
691                             GtkMenu *menu,
692                             gpointer data)
693 {
694 	ESpellChecker *spell_checker;
695 	gint start, end;
696 	gchar *word;
697 
698 	spell_checker = e_spell_entry_get_spell_checker (entry);
699 	if (e_spell_checker_count_active_languages (spell_checker) == 0)
700 		return;
701 
702 	get_word_extents_from_position (
703 		entry, &start, &end, entry->priv->mark_character);
704 	if (start == end)
705 		return;
706 
707 	if (!word_misspelled (entry, start, end))
708 		return;
709 
710 	word = spell_entry_get_chars_from_byte_pos (entry, start, end);
711 	g_return_if_fail (word != NULL);
712 
713 	spell_entry_add_suggestions_menu (entry, menu, word);
714 
715 	g_free (word);
716 }
717 
718 static void
spell_entry_changed(GtkEditable * editable)719 spell_entry_changed (GtkEditable *editable)
720 {
721 	ESpellEntry *entry = E_SPELL_ENTRY (editable);
722 	ESpellChecker *spell_checker;
723 
724 	spell_checker = e_spell_entry_get_spell_checker (entry);
725 	if (e_spell_checker_count_active_languages (spell_checker) == 0)
726 		return;
727 
728 	if (entry->priv->words != NULL) {
729 		g_strfreev (entry->priv->words);
730 		g_free (entry->priv->word_starts);
731 		g_free (entry->priv->word_ends);
732 	}
733 
734 	entry_strsplit_utf8 (
735 		entry,
736 		&entry->priv->words,
737 		&entry->priv->word_starts,
738 		&entry->priv->word_ends);
739 
740 	spell_entry_recheck_all (entry);
741 }
742 
743 static void
spell_entry_notify_scroll_offset(ESpellEntry * spell_entry)744 spell_entry_notify_scroll_offset (ESpellEntry *spell_entry)
745 {
746 	g_object_get (
747 		G_OBJECT (spell_entry), "scroll-offset",
748 		&spell_entry->priv->entry_scroll_offset, NULL);
749 }
750 
751 static gint
spell_entry_find_position(ESpellEntry * spell_entry,gint x)752 spell_entry_find_position (ESpellEntry *spell_entry,
753                            gint x)
754 {
755 	PangoLayout *layout;
756 	PangoLayoutLine *line;
757 	gint index;
758 	gint pos;
759 	gint trailing;
760 	const gchar *text;
761 	GtkEntry *entry = GTK_ENTRY (spell_entry);
762 
763 	layout = gtk_entry_get_layout (entry);
764 	text = pango_layout_get_text (layout);
765 
766 	line = pango_layout_get_lines_readonly (layout)->data;
767 	pango_layout_line_x_to_index (line, x * PANGO_SCALE, &index, &trailing);
768 
769 	pos = g_utf8_pointer_to_offset (text, text + index);
770 	pos += trailing;
771 
772 	return pos;
773 }
774 
775 static void
spell_entry_active_languages_cb(ESpellChecker * spell_checker,GParamSpec * pspec,ESpellEntry * spell_entry)776 spell_entry_active_languages_cb (ESpellChecker *spell_checker,
777                                  GParamSpec *pspec,
778                                  ESpellEntry *spell_entry)
779 {
780 	if (gtk_widget_get_realized (GTK_WIDGET (spell_entry)))
781 		spell_entry_recheck_all (spell_entry);
782 }
783 
784 static void
spell_entry_preedit_changed_cb(ESpellEntry * spell_entry,const gchar * preedit_text,gpointer user_data)785 spell_entry_preedit_changed_cb (ESpellEntry *spell_entry,
786 				const gchar *preedit_text,
787 				gpointer user_data)
788 {
789 	g_return_if_fail (E_IS_SPELL_ENTRY (spell_entry));
790 
791 	spell_entry->priv->im_in_preedit = preedit_text && *preedit_text;
792 }
793 
794 static void
spell_entry_set_property(GObject * object,guint property_id,const GValue * value,GParamSpec * pspec)795 spell_entry_set_property (GObject *object,
796                           guint property_id,
797                           const GValue *value,
798                           GParamSpec *pspec)
799 {
800 	switch (property_id) {
801 		case PROP_CHECKING_ENABLED:
802 			e_spell_entry_set_checking_enabled (
803 				E_SPELL_ENTRY (object),
804 				g_value_get_boolean (value));
805 			return;
806 
807 		case PROP_SPELL_CHECKER:
808 			e_spell_entry_set_spell_checker (
809 				E_SPELL_ENTRY (object),
810 				g_value_get_object (value));
811 			return;
812 	}
813 
814 	G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
815 }
816 
817 static void
spell_entry_get_property(GObject * object,guint property_id,GValue * value,GParamSpec * pspec)818 spell_entry_get_property (GObject *object,
819                                   guint property_id,
820                                   GValue *value,
821                                   GParamSpec *pspec)
822 {
823 	switch (property_id) {
824 		case PROP_CHECKING_ENABLED:
825 			g_value_set_boolean (
826 				value,
827 				e_spell_entry_get_checking_enabled (
828 				E_SPELL_ENTRY (object)));
829 			return;
830 
831 		case PROP_SPELL_CHECKER:
832 			g_value_set_object (
833 				value,
834 				e_spell_entry_get_spell_checker (
835 				E_SPELL_ENTRY (object)));
836 			return;
837 	}
838 
839 	G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
840 }
841 
842 static void
spell_entry_dispose(GObject * object)843 spell_entry_dispose (GObject *object)
844 {
845 	ESpellEntryPrivate *priv;
846 
847 	priv = E_SPELL_ENTRY_GET_PRIVATE (object);
848 
849 	if (priv->active_languages_handler_id > 0) {
850 		g_signal_handler_disconnect (
851 			priv->spell_checker,
852 			priv->active_languages_handler_id);
853 		priv->active_languages_handler_id = 0;
854 	}
855 
856 	g_clear_object (&priv->spell_checker);
857 
858 	g_clear_pointer (&priv->attr_list, pango_attr_list_unref);
859 
860 	/* Chain up to parent's dispose() method. */
861 	G_OBJECT_CLASS (e_spell_entry_parent_class)->dispose (object);
862 }
863 
864 static void
spell_entry_finalize(GObject * object)865 spell_entry_finalize (GObject *object)
866 {
867 	ESpellEntryPrivate *priv;
868 
869 	priv = E_SPELL_ENTRY_GET_PRIVATE (object);
870 
871 	g_strfreev (priv->words);
872 	g_free (priv->word_starts);
873 	g_free (priv->word_ends);
874 
875 	/* Chain up to parent's finalize() method. */
876 	G_OBJECT_CLASS (e_spell_entry_parent_class)->finalize (object);
877 }
878 
879 static void
spell_entry_constructed(GObject * object)880 spell_entry_constructed (GObject *object)
881 {
882 	ESpellEntry *spell_entry;
883 	ESpellChecker *spell_checker;
884 
885 	spell_entry = E_SPELL_ENTRY (object);
886 
887 	/* Chain up to parent's constructed() method. */
888 	G_OBJECT_CLASS (e_spell_entry_parent_class)->constructed (object);
889 
890 	g_signal_connect (spell_entry, "preedit-changed", G_CALLBACK (spell_entry_preedit_changed_cb), NULL);
891 
892 	/* Install a default spell checker if there is not one already. */
893 	spell_checker = e_spell_entry_get_spell_checker (spell_entry);
894 	if (spell_checker == NULL) {
895 		spell_checker = e_spell_checker_new ();
896 		e_spell_entry_set_spell_checker (spell_entry, spell_checker);
897 		g_object_unref (spell_checker);
898 	}
899 
900 	e_extensible_load_extensions (E_EXTENSIBLE (object));
901 }
902 
903 static gboolean
spell_entry_draw(GtkWidget * widget,cairo_t * cr)904 spell_entry_draw (GtkWidget *widget,
905                   cairo_t *cr)
906 {
907 	ESpellEntry *spell_entry = E_SPELL_ENTRY (widget);
908 
909 	if (!spell_entry->priv->im_in_preedit) {
910 		GtkEntry *entry = GTK_ENTRY (widget);
911 		PangoLayout *layout;
912 
913 		layout = gtk_entry_get_layout (entry);
914 		pango_layout_set_attributes (layout, spell_entry->priv->attr_list);
915 	}
916 
917 	/* Chain up to parent's draw() method. */
918 	return GTK_WIDGET_CLASS (e_spell_entry_parent_class)->
919 		draw (widget, cr);
920 }
921 
922 static gboolean
spell_entry_button_press(GtkWidget * widget,GdkEventButton * event)923 spell_entry_button_press (GtkWidget *widget,
924                           GdkEventButton *event)
925 {
926 	ESpellEntry *spell_entry = E_SPELL_ENTRY (widget);
927 
928 	spell_entry->priv->mark_character = spell_entry_find_position (
929 		spell_entry, event->x + spell_entry->priv->entry_scroll_offset);
930 
931 	/* Chain up to parent's button_press_event() method. */
932 	return GTK_WIDGET_CLASS (e_spell_entry_parent_class)->
933 		button_press_event (widget, event);
934 }
935 
936 static void
e_spell_entry_class_init(ESpellEntryClass * class)937 e_spell_entry_class_init (ESpellEntryClass *class)
938 {
939 	GObjectClass *object_class;
940 	GtkWidgetClass *widget_class;
941 
942 	g_type_class_add_private (class, sizeof (ESpellEntryPrivate));
943 
944 	object_class = G_OBJECT_CLASS (class);
945 	object_class->set_property = spell_entry_set_property;
946 	object_class->get_property = spell_entry_get_property;
947 	object_class->dispose = spell_entry_dispose;
948 	object_class->finalize = spell_entry_finalize;
949 	object_class->constructed = spell_entry_constructed;
950 
951 	widget_class = GTK_WIDGET_CLASS (class);
952 	widget_class->draw = spell_entry_draw;
953 	widget_class->button_press_event = spell_entry_button_press;
954 
955 	g_object_class_install_property (
956 		object_class,
957 		PROP_CHECKING_ENABLED,
958 		g_param_spec_boolean (
959 			"checking-enabled",
960 			"checking enabled",
961 			"Spell Checking is Enabled",
962 			TRUE,
963 			G_PARAM_READWRITE |
964 			G_PARAM_STATIC_STRINGS));
965 
966 	g_object_class_install_property (
967 		object_class,
968 		PROP_SPELL_CHECKER,
969 		g_param_spec_object (
970 			"spell-checker",
971 			"Spell Checker",
972 			"The spell checker object",
973 			E_TYPE_SPELL_CHECKER,
974 			G_PARAM_READWRITE |
975 			G_PARAM_STATIC_STRINGS));
976 }
977 
978 static void
e_spell_entry_init(ESpellEntry * spell_entry)979 e_spell_entry_init (ESpellEntry *spell_entry)
980 {
981 	spell_entry->priv = E_SPELL_ENTRY_GET_PRIVATE (spell_entry);
982 	spell_entry->priv->attr_list = pango_attr_list_new ();
983 	spell_entry->priv->checking_enabled = TRUE;
984 	spell_entry->priv->im_in_preedit = FALSE;
985 
986 	g_signal_connect (
987 		spell_entry, "popup-menu",
988 		G_CALLBACK (spell_entry_popup_menu), NULL);
989 	g_signal_connect (
990 		spell_entry, "populate-popup",
991 		G_CALLBACK (spell_entry_populate_popup), NULL);
992 	g_signal_connect (
993 		spell_entry, "changed",
994 		G_CALLBACK (spell_entry_changed), NULL);
995 	e_signal_connect_notify (
996 		spell_entry, "notify::scroll-offset",
997 		G_CALLBACK (spell_entry_notify_scroll_offset), NULL);
998 }
999 
1000 GtkWidget *
e_spell_entry_new(void)1001 e_spell_entry_new (void)
1002 {
1003 	return g_object_new (E_TYPE_SPELL_ENTRY, NULL);
1004 }
1005 
1006 gboolean
e_spell_entry_get_checking_enabled(ESpellEntry * spell_entry)1007 e_spell_entry_get_checking_enabled (ESpellEntry *spell_entry)
1008 {
1009 	g_return_val_if_fail (E_IS_SPELL_ENTRY (spell_entry), FALSE);
1010 
1011 	return spell_entry->priv->checking_enabled;
1012 }
1013 
1014 void
e_spell_entry_set_checking_enabled(ESpellEntry * spell_entry,gboolean enable_checking)1015 e_spell_entry_set_checking_enabled (ESpellEntry *spell_entry,
1016                                     gboolean enable_checking)
1017 {
1018 	g_return_if_fail (E_IS_SPELL_ENTRY (spell_entry));
1019 
1020 	if (spell_entry->priv->checking_enabled == enable_checking)
1021 		return;
1022 
1023 	spell_entry->priv->checking_enabled = enable_checking;
1024 	spell_entry_recheck_all (spell_entry);
1025 
1026 	g_object_notify (G_OBJECT (spell_entry), "checking-enabled");
1027 }
1028 
1029 /**
1030  * e_spell_entry_get_spell_checker:
1031  * @spell_entry: an #ESpellEntry
1032  *
1033  * Returns the #ESpellChecker being used for spell checking.  By default,
1034  * #ESpellEntry creates its own #ESpellChecker, but this can be overridden
1035  * through e_spell_entry_set_spell_checker().
1036  *
1037  * Returns: an #ESpellChecker
1038  **/
1039 ESpellChecker *
e_spell_entry_get_spell_checker(ESpellEntry * spell_entry)1040 e_spell_entry_get_spell_checker (ESpellEntry *spell_entry)
1041 {
1042 	g_return_val_if_fail (E_IS_SPELL_ENTRY (spell_entry), NULL);
1043 
1044 	return spell_entry->priv->spell_checker;
1045 }
1046 
1047 /**
1048  * e_spell_entry_set_spell_checker:
1049  * @spell_entry: an #ESpellEntry
1050  * @spell_checker: an #ESpellChecker
1051  *
1052  * Sets the #ESpellChecker to use for spell checking.  By default,
1053  * #ESpellEntry creates its own #ESpellChecker.  This function can be
1054  * useful for sharing an #ESpellChecker across multiple spell-checking
1055  * widgets, so the active spell checking languages stay synchronized.
1056  **/
1057 void
e_spell_entry_set_spell_checker(ESpellEntry * spell_entry,ESpellChecker * spell_checker)1058 e_spell_entry_set_spell_checker (ESpellEntry *spell_entry,
1059                                  ESpellChecker *spell_checker)
1060 {
1061 	gulong handler_id;
1062 
1063 	g_return_if_fail (E_IS_SPELL_ENTRY (spell_entry));
1064 	g_return_if_fail (E_IS_SPELL_CHECKER (spell_checker));
1065 
1066 	if (spell_checker == spell_entry->priv->spell_checker)
1067 		return;
1068 
1069 	if (spell_entry->priv->spell_checker != NULL) {
1070 		g_signal_handler_disconnect (
1071 			spell_entry->priv->spell_checker,
1072 			spell_entry->priv->active_languages_handler_id);
1073 		g_object_unref (spell_entry->priv->spell_checker);
1074 	}
1075 
1076 	spell_entry->priv->spell_checker = g_object_ref (spell_checker);
1077 
1078 	handler_id = g_signal_connect (
1079 		spell_checker, "notify::active-languages",
1080 		G_CALLBACK (spell_entry_active_languages_cb),
1081 		spell_entry);
1082 
1083 	spell_entry->priv->active_languages_handler_id = handler_id;
1084 
1085 	g_object_notify (G_OBJECT (spell_entry), "spell-checker");
1086 
1087 	if (gtk_widget_get_realized (GTK_WIDGET (spell_entry)))
1088 		spell_entry_recheck_all (spell_entry);
1089 }
1090 
1091