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