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