1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
3 * pluma-automatic-spell-checker.c
4 * This file is part of pluma
5 *
6 * Copyright (C) 2002 Paolo Maggi
7 * Copyright (C) 2012-2021 MATE Developers
8 *
9 * This program is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 2 of the License, or
12 * (at your option) any later version.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program; if not, write to the Free Software
21 * Foundation, Inc., 51 Franklin St, Fifth Floor,
22 * Boston, MA 02110-1301, USA.
23 */
24
25 /*
26 * Modified by the pluma Team, 2002. See the AUTHORS file for a
27 * list of people on the pluma Team.
28 * See the ChangeLog files for a list of changes.
29 */
30
31 /* This is a modified version of gtkspell 2.0.5 (gtkspell.sf.net) */
32 /* gtkspell - a spell-checking addon for GTK's TextView widget
33 * Copyright (c) 2002 Evan Martin.
34 */
35
36 #ifdef HAVE_CONFIG_H
37 #include <config.h>
38 #endif
39
40 #include <string.h>
41
42 #include <glib/gi18n.h>
43
44 #include "pluma-automatic-spell-checker.h"
45 #include "pluma-spell-utils.h"
46
47 struct _PlumaAutomaticSpellChecker {
48 PlumaDocument *doc;
49 GSList *views;
50
51 GtkTextMark *mark_insert_start;
52 GtkTextMark *mark_insert_end;
53 gboolean deferred_check;
54
55 GtkTextTag *tag_highlight;
56 GtkTextMark *mark_click;
57
58 PlumaSpellChecker *spell_checker;
59 };
60
61 static GQuark automatic_spell_checker_id = 0;
62 static GQuark suggestion_id = 0;
63
64 static void pluma_automatic_spell_checker_free_internal (PlumaAutomaticSpellChecker *spell);
65
66 static void
view_destroy(PlumaView * view,PlumaAutomaticSpellChecker * spell)67 view_destroy (PlumaView *view, PlumaAutomaticSpellChecker *spell)
68 {
69 pluma_automatic_spell_checker_detach_view (spell, view);
70 }
71
72 static void
check_word(PlumaAutomaticSpellChecker * spell,GtkTextIter * start,GtkTextIter * end)73 check_word (PlumaAutomaticSpellChecker *spell, GtkTextIter *start, GtkTextIter *end)
74 {
75 gchar *word;
76
77 word = gtk_text_buffer_get_text (GTK_TEXT_BUFFER (spell->doc), start, end, FALSE);
78
79 /*
80 g_print ("Check word: %s [%d - %d]\n", word, gtk_text_iter_get_offset (start),
81 gtk_text_iter_get_offset (end));
82 */
83
84 if (!pluma_spell_checker_check_word (spell->spell_checker, word, -1))
85 {
86 /*
87 g_print ("Apply tag: [%d - %d]\n", gtk_text_iter_get_offset (start),
88 gtk_text_iter_get_offset (end));
89 */
90 gtk_text_buffer_apply_tag (GTK_TEXT_BUFFER (spell->doc),
91 spell->tag_highlight,
92 start,
93 end);
94 }
95
96 g_free (word);
97 }
98
99 static void
check_range(PlumaAutomaticSpellChecker * spell,GtkTextIter start,GtkTextIter end,gboolean force_all)100 check_range (PlumaAutomaticSpellChecker *spell,
101 GtkTextIter start,
102 GtkTextIter end,
103 gboolean force_all)
104 {
105 /* we need to "split" on word boundaries.
106 * luckily, Pango knows what "words" are
107 * so we don't have to figure it out. */
108
109 GtkTextIter wstart;
110 GtkTextIter wend;
111 GtkTextIter cursor;
112 GtkTextIter precursor;
113 gboolean highlight;
114
115 /*
116 g_print ("Check range: [%d - %d]\n", gtk_text_iter_get_offset (&start),
117 gtk_text_iter_get_offset (&end));
118 */
119
120 if (gtk_text_iter_inside_word (&end))
121 gtk_text_iter_forward_word_end (&end);
122
123 if (!gtk_text_iter_starts_word (&start))
124 {
125 if (gtk_text_iter_inside_word (&start) ||
126 gtk_text_iter_ends_word (&start))
127 {
128 gtk_text_iter_backward_word_start (&start);
129 }
130 else
131 {
132 /* if we're neither at the beginning nor inside a word,
133 * me must be in some spaces.
134 * skip forward to the beginning of the next word. */
135
136 if (gtk_text_iter_forward_word_end (&start))
137 gtk_text_iter_backward_word_start (&start);
138 }
139 }
140
141 gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (spell->doc),
142 &cursor,
143 gtk_text_buffer_get_insert (GTK_TEXT_BUFFER (spell->doc)));
144
145 precursor = cursor;
146 gtk_text_iter_backward_char (&precursor);
147
148 highlight = gtk_text_iter_has_tag (&cursor, spell->tag_highlight) ||
149 gtk_text_iter_has_tag (&precursor, spell->tag_highlight);
150
151 gtk_text_buffer_remove_tag (GTK_TEXT_BUFFER (spell->doc),
152 spell->tag_highlight,
153 &start,
154 &end);
155
156 /* Fix a corner case when replacement occurs at beginning of buffer:
157 * An iter at offset 0 seems to always be inside a word,
158 * even if it's not. Possibly a pango bug.
159 */
160 if (gtk_text_iter_get_offset (&start) == 0)
161 {
162 gtk_text_iter_forward_word_end(&start);
163 gtk_text_iter_backward_word_start(&start);
164 }
165
166 wstart = start;
167
168 while (pluma_spell_utils_skip_no_spell_check (&wstart, &end) &&
169 gtk_text_iter_compare (&wstart, &end) < 0)
170 {
171 gboolean inword;
172
173 /* move wend to the end of the current word. */
174 wend = wstart;
175
176 gtk_text_iter_forward_word_end (&wend);
177
178 inword = (gtk_text_iter_compare (&wstart, &cursor) < 0) &&
179 (gtk_text_iter_compare (&cursor, &wend) <= 0);
180
181 if (inword && !force_all)
182 {
183 /* this word is being actively edited,
184 * only check if it's already highligted,
185 * otherwise defer this check until later. */
186 if (highlight)
187 check_word (spell, &wstart, &wend);
188 else
189 spell->deferred_check = TRUE;
190 }
191 else
192 {
193 check_word (spell, &wstart, &wend);
194 spell->deferred_check = FALSE;
195 }
196
197 /* now move wend to the beginning of the next word, */
198 gtk_text_iter_forward_word_end (&wend);
199 gtk_text_iter_backward_word_start (&wend);
200
201 /* make sure we've actually advanced
202 * (we don't advance in some corner cases), */
203 if (gtk_text_iter_equal (&wstart, &wend))
204 break; /* we're done in these cases.. */
205
206 /* and then pick this as the new next word beginning. */
207 wstart = wend;
208 }
209 }
210
211 static void
check_deferred_range(PlumaAutomaticSpellChecker * spell,gboolean force_all)212 check_deferred_range (PlumaAutomaticSpellChecker *spell,
213 gboolean force_all)
214 {
215 GtkTextIter start, end;
216
217 gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (spell->doc),
218 &start,
219 spell->mark_insert_start);
220 gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (spell->doc),
221 &end,
222 spell->mark_insert_end);
223
224 check_range (spell, start, end, force_all);
225 }
226
227 /* insertion works like this:
228 * - before the text is inserted, we mark the position in the buffer.
229 * - after the text is inserted, we see where our mark is and use that and
230 * the current position to check the entire range of inserted text.
231 *
232 * this may be overkill for the common case (inserting one character). */
233
234 static void
insert_text_before(GtkTextBuffer * buffer,GtkTextIter * iter,gchar * text,gint len,PlumaAutomaticSpellChecker * spell)235 insert_text_before (GtkTextBuffer *buffer, GtkTextIter *iter,
236 gchar *text, gint len, PlumaAutomaticSpellChecker *spell)
237 {
238 gtk_text_buffer_move_mark (buffer, spell->mark_insert_start, iter);
239 }
240
241 static void
insert_text_after(GtkTextBuffer * buffer,GtkTextIter * iter,gchar * text,gint len,PlumaAutomaticSpellChecker * spell)242 insert_text_after (GtkTextBuffer *buffer, GtkTextIter *iter,
243 gchar *text, gint len, PlumaAutomaticSpellChecker *spell)
244 {
245 GtkTextIter start;
246
247 /* we need to check a range of text. */
248 gtk_text_buffer_get_iter_at_mark (buffer, &start, spell->mark_insert_start);
249
250 check_range (spell, start, *iter, FALSE);
251
252 gtk_text_buffer_move_mark (buffer, spell->mark_insert_end, iter);
253 }
254
255 /* deleting is more simple: we're given the range of deleted text.
256 * after deletion, the start and end iters should be at the same position
257 * (because all of the text between them was deleted!).
258 * this means we only really check the words immediately bounding the
259 * deletion.
260 */
261
262 static void
delete_range_after(GtkTextBuffer * buffer,GtkTextIter * start,GtkTextIter * end,PlumaAutomaticSpellChecker * spell)263 delete_range_after (GtkTextBuffer *buffer, GtkTextIter *start, GtkTextIter *end,
264 PlumaAutomaticSpellChecker *spell)
265 {
266 check_range (spell, *start, *end, FALSE);
267 }
268
269 static void
mark_set(GtkTextBuffer * buffer,GtkTextIter * iter,GtkTextMark * mark,PlumaAutomaticSpellChecker * spell)270 mark_set (GtkTextBuffer *buffer,
271 GtkTextIter *iter,
272 GtkTextMark *mark,
273 PlumaAutomaticSpellChecker *spell)
274 {
275 /* if the cursor has moved and there is a deferred check so handle it now */
276 if ((mark == gtk_text_buffer_get_insert (buffer)) && spell->deferred_check)
277 check_deferred_range (spell, FALSE);
278 }
279
280 static void
get_word_extents_from_mark(GtkTextBuffer * buffer,GtkTextIter * start,GtkTextIter * end,GtkTextMark * mark)281 get_word_extents_from_mark (GtkTextBuffer *buffer,
282 GtkTextIter *start,
283 GtkTextIter *end,
284 GtkTextMark *mark)
285 {
286 gtk_text_buffer_get_iter_at_mark(buffer, start, mark);
287
288 if (!gtk_text_iter_starts_word (start))
289 gtk_text_iter_backward_word_start (start);
290
291 *end = *start;
292
293 if (gtk_text_iter_inside_word (end))
294 gtk_text_iter_forward_word_end (end);
295 }
296
297 static void
remove_tag_to_word(PlumaAutomaticSpellChecker * spell,const gchar * word)298 remove_tag_to_word (PlumaAutomaticSpellChecker *spell, const gchar *word)
299 {
300 GtkTextIter iter;
301 GtkTextIter match_start, match_end;
302
303 gboolean found;
304
305 gtk_text_buffer_get_iter_at_offset (GTK_TEXT_BUFFER (spell->doc), &iter, 0);
306
307 found = TRUE;
308
309 while (found)
310 {
311 found = gtk_text_iter_forward_search (&iter,
312 word,
313 GTK_TEXT_SEARCH_VISIBLE_ONLY | GTK_TEXT_SEARCH_TEXT_ONLY,
314 &match_start,
315 &match_end,
316 NULL);
317
318 if (found)
319 {
320 if (gtk_text_iter_starts_word (&match_start) &&
321 gtk_text_iter_ends_word (&match_end))
322 {
323 gtk_text_buffer_remove_tag (GTK_TEXT_BUFFER (spell->doc),
324 spell->tag_highlight,
325 &match_start,
326 &match_end);
327 }
328
329 iter = match_end;
330 }
331 }
332 }
333
334 static void
add_to_dictionary(GtkWidget * menuitem,PlumaAutomaticSpellChecker * spell)335 add_to_dictionary (GtkWidget *menuitem, PlumaAutomaticSpellChecker *spell)
336 {
337 gchar *word;
338
339 GtkTextIter start, end;
340
341 get_word_extents_from_mark (GTK_TEXT_BUFFER (spell->doc), &start, &end, spell->mark_click);
342
343 word = gtk_text_buffer_get_text (GTK_TEXT_BUFFER (spell->doc),
344 &start,
345 &end,
346 FALSE);
347
348 pluma_spell_checker_add_word_to_personal (spell->spell_checker, word, -1);
349
350 g_free (word);
351 }
352
353 static void
ignore_all(GtkWidget * menuitem,PlumaAutomaticSpellChecker * spell)354 ignore_all (GtkWidget *menuitem, PlumaAutomaticSpellChecker *spell)
355 {
356 gchar *word;
357
358 GtkTextIter start, end;
359
360 get_word_extents_from_mark (GTK_TEXT_BUFFER (spell->doc), &start, &end, spell->mark_click);
361
362 word = gtk_text_buffer_get_text (GTK_TEXT_BUFFER (spell->doc),
363 &start,
364 &end,
365 FALSE);
366
367 pluma_spell_checker_add_word_to_session (spell->spell_checker, word, -1);
368
369 g_free (word);
370 }
371
372 static void
replace_word(GtkWidget * menuitem,PlumaAutomaticSpellChecker * spell)373 replace_word (GtkWidget *menuitem, PlumaAutomaticSpellChecker *spell)
374 {
375 gchar *oldword;
376 const gchar *newword;
377
378 GtkTextIter start, end;
379
380 get_word_extents_from_mark (GTK_TEXT_BUFFER (spell->doc), &start, &end, spell->mark_click);
381
382 oldword = gtk_text_buffer_get_text (GTK_TEXT_BUFFER (spell->doc), &start, &end, FALSE);
383
384 newword = g_object_get_qdata (G_OBJECT (menuitem), suggestion_id);
385 g_return_if_fail (newword != NULL);
386
387 gtk_text_buffer_begin_user_action (GTK_TEXT_BUFFER (spell->doc));
388
389 gtk_text_buffer_delete (GTK_TEXT_BUFFER (spell->doc), &start, &end);
390 gtk_text_buffer_insert (GTK_TEXT_BUFFER (spell->doc), &start, newword, -1);
391
392 gtk_text_buffer_end_user_action (GTK_TEXT_BUFFER (spell->doc));
393
394 pluma_spell_checker_set_correction (spell->spell_checker,
395 oldword, strlen (oldword),
396 newword, strlen (newword));
397
398 g_free (oldword);
399 }
400
401 static GtkWidget *
build_suggestion_menu(PlumaAutomaticSpellChecker * spell,const gchar * word)402 build_suggestion_menu (PlumaAutomaticSpellChecker *spell, const gchar *word)
403 {
404 GtkWidget *topmenu, *menu;
405 GtkWidget *mi;
406 GSList *suggestions;
407 GSList *list;
408 gchar *label_text;
409
410 topmenu = menu = gtk_menu_new();
411
412 suggestions = pluma_spell_checker_get_suggestions (spell->spell_checker, word, -1);
413
414 list = suggestions;
415
416 if (suggestions == NULL)
417 {
418 /* no suggestions. put something in the menu anyway... */
419 GtkWidget *label;
420 /* Translators: Displayed in the "Check Spelling" dialog if there are no suggestions for the current misspelled word */
421 label = gtk_label_new (_("(no suggested words)"));
422
423 mi = gtk_menu_item_new ();
424 gtk_widget_set_sensitive (mi, FALSE);
425 gtk_container_add (GTK_CONTAINER(mi), label);
426 gtk_widget_show_all (mi);
427 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), mi);
428 }
429 else
430 {
431 gint count = 0;
432
433 /* build a set of menus with suggestions. */
434 while (suggestions != NULL)
435 {
436 GtkWidget *label;
437
438 if (count == 10)
439 {
440 /* Separator */
441 mi = gtk_menu_item_new ();
442 gtk_widget_show (mi);
443 gtk_menu_shell_append (GTK_MENU_SHELL (menu), mi);
444
445 mi = gtk_menu_item_new_with_mnemonic (_("_More..."));
446 gtk_widget_show (mi);
447 gtk_menu_shell_append (GTK_MENU_SHELL (menu), mi);
448
449 menu = gtk_menu_new ();
450 gtk_menu_item_set_submenu (GTK_MENU_ITEM (mi), menu);
451 count = 0;
452 }
453
454 label_text = g_strdup_printf ("<b>%s</b>", (gchar*) suggestions->data);
455
456 label = gtk_label_new (label_text);
457 gtk_label_set_use_markup (GTK_LABEL (label), TRUE);
458 gtk_label_set_xalign (GTK_LABEL (label), 0.0);
459
460 mi = gtk_menu_item_new ();
461 gtk_container_add (GTK_CONTAINER(mi), label);
462
463 gtk_widget_show_all (mi);
464 gtk_menu_shell_append (GTK_MENU_SHELL (menu), mi);
465
466 g_object_set_qdata_full (G_OBJECT (mi),
467 suggestion_id,
468 g_strdup (suggestions->data),
469 (GDestroyNotify)g_free);
470
471 g_free (label_text);
472 g_signal_connect (mi,
473 "activate",
474 G_CALLBACK (replace_word),
475 spell);
476
477 count++;
478
479 suggestions = g_slist_next (suggestions);
480 }
481 }
482
483 /* free the suggestion list */
484 suggestions = list;
485
486 while (list)
487 {
488 g_free (list->data);
489 list = g_slist_next (list);
490 }
491
492 g_slist_free (suggestions);
493
494 /* Separator */
495 mi = gtk_menu_item_new ();
496 gtk_widget_show (mi);
497 gtk_menu_shell_append (GTK_MENU_SHELL (topmenu), mi);
498
499 /* Ignore all */
500 mi = gtk_image_menu_item_new_with_mnemonic (_("_Ignore All"));
501 gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (mi),
502 gtk_image_new_from_icon_name ("go-bottom",
503 GTK_ICON_SIZE_MENU));
504
505 g_signal_connect (mi,
506 "activate",
507 G_CALLBACK(ignore_all),
508 spell);
509
510 gtk_widget_show_all (mi);
511
512 gtk_menu_shell_append (GTK_MENU_SHELL (topmenu), mi);
513
514 /* + Add to Dictionary */
515 mi = gtk_image_menu_item_new_with_mnemonic (_("_Add"));
516 gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (mi),
517 gtk_image_new_from_icon_name ("list-add",
518 GTK_ICON_SIZE_MENU));
519
520 g_signal_connect (mi,
521 "activate",
522 G_CALLBACK (add_to_dictionary),
523 spell);
524
525 gtk_widget_show_all (mi);
526
527 gtk_menu_shell_append (GTK_MENU_SHELL (topmenu), mi);
528
529 return topmenu;
530 }
531
532 static void
populate_popup(GtkTextView * textview,GtkMenu * menu,PlumaAutomaticSpellChecker * spell)533 populate_popup (GtkTextView *textview, GtkMenu *menu, PlumaAutomaticSpellChecker *spell)
534 {
535 GtkWidget *img, *mi;
536 GtkTextIter start, end;
537 char *word;
538
539 /* we need to figure out if they picked a misspelled word. */
540 get_word_extents_from_mark (GTK_TEXT_BUFFER (spell->doc), &start, &end, spell->mark_click);
541
542 /* if our highlight algorithm ever messes up,
543 * this isn't correct, either. */
544 if (!gtk_text_iter_has_tag (&start, spell->tag_highlight))
545 return; /* word wasn't misspelled. */
546
547 /* menu separator comes first. */
548 mi = gtk_menu_item_new ();
549 gtk_widget_show (mi);
550 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), mi);
551
552 /* then, on top of it, the suggestions menu. */
553 img = gtk_image_new_from_icon_name ("tools-check-spelling", GTK_ICON_SIZE_MENU);
554 mi = gtk_image_menu_item_new_with_mnemonic (_("_Spelling Suggestions..."));
555 gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (mi), img);
556
557 word = gtk_text_buffer_get_text (GTK_TEXT_BUFFER (spell->doc), &start, &end, FALSE);
558 gtk_menu_item_set_submenu (GTK_MENU_ITEM (mi),
559 build_suggestion_menu (spell, word));
560 g_free(word);
561
562 gtk_widget_show_all (mi);
563 gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), mi);
564 }
565
566 void
pluma_automatic_spell_checker_recheck_all(PlumaAutomaticSpellChecker * spell)567 pluma_automatic_spell_checker_recheck_all (PlumaAutomaticSpellChecker *spell)
568 {
569 GtkTextIter start, end;
570
571 g_return_if_fail (spell != NULL);
572
573 gtk_text_buffer_get_bounds (GTK_TEXT_BUFFER (spell->doc), &start, &end);
574
575 check_range (spell, start, end, TRUE);
576 }
577
578 static void
add_word_signal_cb(PlumaSpellChecker * checker,const gchar * word,gint len,PlumaAutomaticSpellChecker * spell)579 add_word_signal_cb (PlumaSpellChecker *checker,
580 const gchar *word,
581 gint len,
582 PlumaAutomaticSpellChecker *spell)
583 {
584 gchar *w;
585
586 if (len < 0)
587 w = g_strdup (word);
588 else
589 w = g_strndup (word, len);
590
591 remove_tag_to_word (spell, w);
592
593 g_free (w);
594 }
595
596 static void
set_language_cb(PlumaSpellChecker * checker,const PlumaSpellCheckerLanguage * lang,PlumaAutomaticSpellChecker * spell)597 set_language_cb (PlumaSpellChecker *checker,
598 const PlumaSpellCheckerLanguage *lang,
599 PlumaAutomaticSpellChecker *spell)
600 {
601 pluma_automatic_spell_checker_recheck_all (spell);
602 }
603
604 static void
clear_session_cb(PlumaSpellChecker * checker,PlumaAutomaticSpellChecker * spell)605 clear_session_cb (PlumaSpellChecker *checker,
606 PlumaAutomaticSpellChecker *spell)
607 {
608 pluma_automatic_spell_checker_recheck_all (spell);
609 }
610
611 /* When the user right-clicks on a word, they want to check that word.
612 * Here, we do NOT move the cursor to the location of the clicked-upon word
613 * since that prevents the use of edit functions on the context menu.
614 */
615 static gboolean
button_press_event(GtkTextView * view,GdkEventButton * event,PlumaAutomaticSpellChecker * spell)616 button_press_event (GtkTextView *view,
617 GdkEventButton *event,
618 PlumaAutomaticSpellChecker *spell)
619 {
620 if (event->button == 3)
621 {
622 gint x, y;
623 GtkTextIter iter;
624
625 GtkTextBuffer *buffer = gtk_text_view_get_buffer (view);
626
627 /* handle deferred check if it exists */
628 if (spell->deferred_check)
629 check_deferred_range (spell, TRUE);
630
631 gtk_text_view_window_to_buffer_coords (view,
632 GTK_TEXT_WINDOW_TEXT,
633 event->x, event->y,
634 &x, &y);
635
636 gtk_text_view_get_iter_at_location (view, &iter, x, y);
637
638 gtk_text_buffer_move_mark (buffer, spell->mark_click, &iter);
639 }
640
641 return FALSE; /* false: let gtk process this event, too.
642 we don't want to eat any events. */
643 }
644
645 /* Move the insert mark before popping up the menu, otherwise it
646 * will contain the wrong set of suggestions.
647 */
648 static gboolean
popup_menu_event(GtkTextView * view,PlumaAutomaticSpellChecker * spell)649 popup_menu_event (GtkTextView *view, PlumaAutomaticSpellChecker *spell)
650 {
651 GtkTextIter iter;
652 GtkTextBuffer *buffer;
653
654 buffer = gtk_text_view_get_buffer (view);
655
656 /* handle deferred check if it exists */
657 if (spell->deferred_check)
658 check_deferred_range (spell, TRUE);
659
660 gtk_text_buffer_get_iter_at_mark (buffer, &iter,
661 gtk_text_buffer_get_insert (buffer));
662 gtk_text_buffer_move_mark (buffer, spell->mark_click, &iter);
663
664 return FALSE;
665 }
666
667 static void
tag_table_changed(GtkTextTagTable * table,PlumaAutomaticSpellChecker * spell)668 tag_table_changed (GtkTextTagTable *table,
669 PlumaAutomaticSpellChecker *spell)
670 {
671 g_return_if_fail (spell->tag_highlight != NULL);
672
673 gtk_text_tag_set_priority (spell->tag_highlight,
674 gtk_text_tag_table_get_size (table) - 1);
675 }
676
677 static void
tag_added_or_removed(GtkTextTagTable * table,GtkTextTag * tag,PlumaAutomaticSpellChecker * spell)678 tag_added_or_removed (GtkTextTagTable *table,
679 GtkTextTag *tag,
680 PlumaAutomaticSpellChecker *spell)
681 {
682 tag_table_changed (table, spell);
683 }
684
685 static void
tag_changed(GtkTextTagTable * table,GtkTextTag * tag,gboolean size_changed,PlumaAutomaticSpellChecker * spell)686 tag_changed (GtkTextTagTable *table,
687 GtkTextTag *tag,
688 gboolean size_changed,
689 PlumaAutomaticSpellChecker *spell)
690 {
691 tag_table_changed (table, spell);
692 }
693
694 static void
highlight_updated(GtkSourceBuffer * buffer,GtkTextIter * start,GtkTextIter * end,PlumaAutomaticSpellChecker * spell)695 highlight_updated (GtkSourceBuffer *buffer,
696 GtkTextIter *start,
697 GtkTextIter *end,
698 PlumaAutomaticSpellChecker *spell)
699 {
700 check_range (spell, *start, *end, FALSE);
701 }
702
703 static void
spell_tag_destroyed(PlumaAutomaticSpellChecker * spell,GObject * where_the_object_was)704 spell_tag_destroyed (PlumaAutomaticSpellChecker *spell,
705 GObject *where_the_object_was)
706 {
707 spell->tag_highlight = NULL;
708 }
709
710 PlumaAutomaticSpellChecker *
pluma_automatic_spell_checker_new(PlumaDocument * doc,PlumaSpellChecker * checker)711 pluma_automatic_spell_checker_new (PlumaDocument *doc,
712 PlumaSpellChecker *checker)
713 {
714 PlumaAutomaticSpellChecker *spell;
715 GtkTextTagTable *tag_table;
716 GtkTextIter start, end;
717
718 g_return_val_if_fail (PLUMA_IS_DOCUMENT (doc), NULL);
719 g_return_val_if_fail (PLUMA_IS_SPELL_CHECKER (checker), NULL);
720 g_return_val_if_fail ((spell = pluma_automatic_spell_checker_get_from_document (doc)) == NULL,
721 spell);
722
723 /* attach to the widget */
724 spell = g_new0 (PlumaAutomaticSpellChecker, 1);
725
726 spell->doc = doc;
727 spell->spell_checker = g_object_ref (checker);
728
729 if (automatic_spell_checker_id == 0)
730 {
731 automatic_spell_checker_id =
732 g_quark_from_string ("PlumaAutomaticSpellCheckerID");
733 }
734 if (suggestion_id == 0)
735 {
736 suggestion_id = g_quark_from_string ("PlumaAutoSuggestionID");
737 }
738
739 g_object_set_qdata_full (G_OBJECT (doc),
740 automatic_spell_checker_id,
741 spell,
742 (GDestroyNotify)pluma_automatic_spell_checker_free_internal);
743
744 g_signal_connect (doc,
745 "insert-text",
746 G_CALLBACK (insert_text_before),
747 spell);
748 g_signal_connect_after (doc,
749 "insert-text",
750 G_CALLBACK (insert_text_after),
751 spell);
752 g_signal_connect_after (doc,
753 "delete-range",
754 G_CALLBACK (delete_range_after),
755 spell);
756 g_signal_connect (doc,
757 "mark-set",
758 G_CALLBACK (mark_set),
759 spell);
760
761 g_signal_connect (doc,
762 "highlight-updated",
763 G_CALLBACK (highlight_updated),
764 spell);
765
766 g_signal_connect (spell->spell_checker,
767 "add_word_to_session",
768 G_CALLBACK (add_word_signal_cb),
769 spell);
770 g_signal_connect (spell->spell_checker,
771 "add_word_to_personal",
772 G_CALLBACK (add_word_signal_cb),
773 spell);
774 g_signal_connect (spell->spell_checker,
775 "clear_session",
776 G_CALLBACK (clear_session_cb),
777 spell);
778 g_signal_connect (spell->spell_checker,
779 "set_language",
780 G_CALLBACK (set_language_cb),
781 spell);
782
783 spell->tag_highlight = gtk_text_buffer_create_tag (
784 GTK_TEXT_BUFFER (doc),
785 "gtkspell-misspelled",
786 "underline", PANGO_UNDERLINE_ERROR,
787 NULL);
788
789 g_object_weak_ref (G_OBJECT (spell->tag_highlight),
790 (GWeakNotify)spell_tag_destroyed,
791 spell);
792
793 tag_table = gtk_text_buffer_get_tag_table (GTK_TEXT_BUFFER (doc));
794
795 gtk_text_tag_set_priority (spell->tag_highlight,
796 gtk_text_tag_table_get_size (tag_table) - 1);
797
798 g_signal_connect (tag_table,
799 "tag-added",
800 G_CALLBACK (tag_added_or_removed),
801 spell);
802 g_signal_connect (tag_table,
803 "tag-removed",
804 G_CALLBACK (tag_added_or_removed),
805 spell);
806 g_signal_connect (tag_table,
807 "tag-changed",
808 G_CALLBACK (tag_changed),
809 spell);
810
811 /* we create the mark here, but we don't use it until text is
812 * inserted, so we don't really care where iter points. */
813 gtk_text_buffer_get_bounds (GTK_TEXT_BUFFER (doc), &start, &end);
814
815 spell->mark_insert_start = gtk_text_buffer_get_mark (GTK_TEXT_BUFFER (doc),
816 "pluma-automatic-spell-checker-insert-start");
817
818 if (spell->mark_insert_start == NULL)
819 {
820 spell->mark_insert_start =
821 gtk_text_buffer_create_mark (GTK_TEXT_BUFFER (doc),
822 "pluma-automatic-spell-checker-insert-start",
823 &start,
824 TRUE);
825 }
826 else
827 {
828 gtk_text_buffer_move_mark (GTK_TEXT_BUFFER (doc),
829 spell->mark_insert_start,
830 &start);
831 }
832
833 spell->mark_insert_end = gtk_text_buffer_get_mark (GTK_TEXT_BUFFER (doc),
834 "pluma-automatic-spell-checker-insert-end");
835
836 if (spell->mark_insert_end == NULL)
837 {
838 spell->mark_insert_end =
839 gtk_text_buffer_create_mark (GTK_TEXT_BUFFER (doc),
840 "pluma-automatic-spell-checker-insert-end",
841 &start,
842 TRUE);
843 }
844 else
845 {
846 gtk_text_buffer_move_mark (GTK_TEXT_BUFFER (doc),
847 spell->mark_insert_end,
848 &start);
849 }
850
851 spell->mark_click = gtk_text_buffer_get_mark (GTK_TEXT_BUFFER (doc),
852 "pluma-automatic-spell-checker-click");
853
854 if (spell->mark_click == NULL)
855 {
856 spell->mark_click =
857 gtk_text_buffer_create_mark (GTK_TEXT_BUFFER (doc),
858 "pluma-automatic-spell-checker-click",
859 &start,
860 TRUE);
861 }
862 else
863 {
864 gtk_text_buffer_move_mark (GTK_TEXT_BUFFER (doc),
865 spell->mark_click,
866 &start);
867 }
868
869 spell->deferred_check = FALSE;
870
871 return spell;
872 }
873
874 PlumaAutomaticSpellChecker *
pluma_automatic_spell_checker_get_from_document(const PlumaDocument * doc)875 pluma_automatic_spell_checker_get_from_document (const PlumaDocument *doc)
876 {
877 g_return_val_if_fail (PLUMA_IS_DOCUMENT (doc), NULL);
878
879 if (automatic_spell_checker_id == 0)
880 return NULL;
881
882 return g_object_get_qdata (G_OBJECT (doc), automatic_spell_checker_id);
883 }
884
885 void
pluma_automatic_spell_checker_free(PlumaAutomaticSpellChecker * spell)886 pluma_automatic_spell_checker_free (PlumaAutomaticSpellChecker *spell)
887 {
888 g_return_if_fail (spell != NULL);
889 g_return_if_fail (pluma_automatic_spell_checker_get_from_document (spell->doc) == spell);
890
891 if (automatic_spell_checker_id == 0)
892 return;
893
894 g_object_set_qdata (G_OBJECT (spell->doc), automatic_spell_checker_id, NULL);
895 }
896
897 static void
pluma_automatic_spell_checker_free_internal(PlumaAutomaticSpellChecker * spell)898 pluma_automatic_spell_checker_free_internal (PlumaAutomaticSpellChecker *spell)
899 {
900 GtkTextTagTable *table;
901 GtkTextIter start, end;
902 GSList *list;
903
904 g_return_if_fail (spell != NULL);
905
906 table = gtk_text_buffer_get_tag_table (GTK_TEXT_BUFFER (spell->doc));
907
908 if (table != NULL && spell->tag_highlight != NULL)
909 {
910 gtk_text_buffer_get_bounds (GTK_TEXT_BUFFER (spell->doc),
911 &start,
912 &end);
913 gtk_text_buffer_remove_tag (GTK_TEXT_BUFFER (spell->doc),
914 spell->tag_highlight,
915 &start,
916 &end);
917
918 g_signal_handlers_disconnect_matched (G_OBJECT (table),
919 G_SIGNAL_MATCH_DATA,
920 0, 0, NULL, NULL,
921 spell);
922
923 gtk_text_tag_table_remove (table, spell->tag_highlight);
924 }
925
926 g_signal_handlers_disconnect_matched (G_OBJECT (spell->doc),
927 G_SIGNAL_MATCH_DATA,
928 0, 0, NULL, NULL,
929 spell);
930
931 g_signal_handlers_disconnect_matched (G_OBJECT (spell->spell_checker),
932 G_SIGNAL_MATCH_DATA,
933 0, 0, NULL, NULL,
934 spell);
935
936 g_object_unref (spell->spell_checker);
937
938 list = spell->views;
939 while (list != NULL)
940 {
941 PlumaView *view = PLUMA_VIEW (list->data);
942
943 g_signal_handlers_disconnect_matched (G_OBJECT (view),
944 G_SIGNAL_MATCH_DATA,
945 0, 0, NULL, NULL,
946 spell);
947
948 g_signal_handlers_disconnect_matched (G_OBJECT (view),
949 G_SIGNAL_MATCH_DATA,
950 0, 0, NULL, NULL,
951 spell);
952
953 list = g_slist_next (list);
954 }
955
956 g_slist_free (spell->views);
957
958 g_free (spell);
959 }
960
961 void
pluma_automatic_spell_checker_attach_view(PlumaAutomaticSpellChecker * spell,PlumaView * view)962 pluma_automatic_spell_checker_attach_view (
963 PlumaAutomaticSpellChecker *spell,
964 PlumaView *view)
965 {
966 g_return_if_fail (spell != NULL);
967 g_return_if_fail (PLUMA_IS_VIEW (view));
968
969 g_return_if_fail (gtk_text_view_get_buffer (GTK_TEXT_VIEW (view)) ==
970 GTK_TEXT_BUFFER (spell->doc));
971
972 g_signal_connect (view,
973 "button-press-event",
974 G_CALLBACK (button_press_event),
975 spell);
976 g_signal_connect (view,
977 "popup-menu",
978 G_CALLBACK (popup_menu_event),
979 spell);
980 g_signal_connect (view,
981 "populate-popup",
982 G_CALLBACK (populate_popup),
983 spell);
984 g_signal_connect (view,
985 "destroy",
986 G_CALLBACK (view_destroy),
987 spell);
988
989 spell->views = g_slist_prepend (spell->views, view);
990 }
991
992 void
pluma_automatic_spell_checker_detach_view(PlumaAutomaticSpellChecker * spell,PlumaView * view)993 pluma_automatic_spell_checker_detach_view (
994 PlumaAutomaticSpellChecker *spell,
995 PlumaView *view)
996 {
997 g_return_if_fail (spell != NULL);
998 g_return_if_fail (PLUMA_IS_VIEW (view));
999
1000 g_return_if_fail (gtk_text_view_get_buffer (GTK_TEXT_VIEW (view)) ==
1001 GTK_TEXT_BUFFER (spell->doc));
1002 g_return_if_fail (spell->views != NULL);
1003
1004 g_signal_handlers_disconnect_matched (G_OBJECT (view),
1005 G_SIGNAL_MATCH_DATA,
1006 0, 0, NULL, NULL,
1007 spell);
1008
1009 g_signal_handlers_disconnect_matched (G_OBJECT (view),
1010 G_SIGNAL_MATCH_DATA,
1011 0, 0, NULL, NULL,
1012 spell);
1013
1014 spell->views = g_slist_remove (spell->views, view);
1015 }
1016
1017