1016 lines
26 KiB
C
Executable File
1016 lines
26 KiB
C
Executable File
/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
|
|
/*
|
|
* pluma-automatic-spell-checker.c
|
|
* This file is part of pluma
|
|
*
|
|
* Copyright (C) 2002 Paolo Maggi
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 59 Temple Place, Suite 330,
|
|
* Boston, MA 02111-1307, USA.
|
|
*/
|
|
|
|
/*
|
|
* Modified by the pluma Team, 2002. See the AUTHORS file for a
|
|
* list of people on the pluma Team.
|
|
* See the ChangeLog files for a list of changes.
|
|
*/
|
|
|
|
/* This is a modified version of gtkspell 2.0.5 (gtkspell.sf.net) */
|
|
/* gtkspell - a spell-checking addon for GTK's TextView widget
|
|
* Copyright (c) 2002 Evan Martin.
|
|
*/
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
#include <config.h>
|
|
#endif
|
|
|
|
#include <string.h>
|
|
|
|
#include <glib/gi18n.h>
|
|
|
|
#include "pluma-automatic-spell-checker.h"
|
|
#include "pluma-spell-utils.h"
|
|
|
|
struct _PlumaAutomaticSpellChecker {
|
|
PlumaDocument *doc;
|
|
GSList *views;
|
|
|
|
GtkTextMark *mark_insert_start;
|
|
GtkTextMark *mark_insert_end;
|
|
gboolean deferred_check;
|
|
|
|
GtkTextTag *tag_highlight;
|
|
GtkTextMark *mark_click;
|
|
|
|
PlumaSpellChecker *spell_checker;
|
|
};
|
|
|
|
static GQuark automatic_spell_checker_id = 0;
|
|
static GQuark suggestion_id = 0;
|
|
|
|
static void pluma_automatic_spell_checker_free_internal (PlumaAutomaticSpellChecker *spell);
|
|
|
|
static void
|
|
view_destroy (PlumaView *view, PlumaAutomaticSpellChecker *spell)
|
|
{
|
|
pluma_automatic_spell_checker_detach_view (spell, view);
|
|
}
|
|
|
|
static void
|
|
check_word (PlumaAutomaticSpellChecker *spell, GtkTextIter *start, GtkTextIter *end)
|
|
{
|
|
gchar *word;
|
|
|
|
word = gtk_text_buffer_get_text (GTK_TEXT_BUFFER (spell->doc), start, end, FALSE);
|
|
|
|
/*
|
|
g_print ("Check word: %s [%d - %d]\n", word, gtk_text_iter_get_offset (start),
|
|
gtk_text_iter_get_offset (end));
|
|
*/
|
|
|
|
if (!pluma_spell_checker_check_word (spell->spell_checker, word, -1))
|
|
{
|
|
/*
|
|
g_print ("Apply tag: [%d - %d]\n", gtk_text_iter_get_offset (start),
|
|
gtk_text_iter_get_offset (end));
|
|
*/
|
|
gtk_text_buffer_apply_tag (GTK_TEXT_BUFFER (spell->doc),
|
|
spell->tag_highlight,
|
|
start,
|
|
end);
|
|
}
|
|
|
|
g_free (word);
|
|
}
|
|
|
|
static void
|
|
check_range (PlumaAutomaticSpellChecker *spell,
|
|
GtkTextIter start,
|
|
GtkTextIter end,
|
|
gboolean force_all)
|
|
{
|
|
/* we need to "split" on word boundaries.
|
|
* luckily, Pango knows what "words" are
|
|
* so we don't have to figure it out. */
|
|
|
|
GtkTextIter wstart;
|
|
GtkTextIter wend;
|
|
GtkTextIter cursor;
|
|
GtkTextIter precursor;
|
|
gboolean highlight;
|
|
|
|
/*
|
|
g_print ("Check range: [%d - %d]\n", gtk_text_iter_get_offset (&start),
|
|
gtk_text_iter_get_offset (&end));
|
|
*/
|
|
|
|
if (gtk_text_iter_inside_word (&end))
|
|
gtk_text_iter_forward_word_end (&end);
|
|
|
|
if (!gtk_text_iter_starts_word (&start))
|
|
{
|
|
if (gtk_text_iter_inside_word (&start) ||
|
|
gtk_text_iter_ends_word (&start))
|
|
{
|
|
gtk_text_iter_backward_word_start (&start);
|
|
}
|
|
else
|
|
{
|
|
/* if we're neither at the beginning nor inside a word,
|
|
* me must be in some spaces.
|
|
* skip forward to the beginning of the next word. */
|
|
|
|
if (gtk_text_iter_forward_word_end (&start))
|
|
gtk_text_iter_backward_word_start (&start);
|
|
}
|
|
}
|
|
|
|
gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (spell->doc),
|
|
&cursor,
|
|
gtk_text_buffer_get_insert (GTK_TEXT_BUFFER (spell->doc)));
|
|
|
|
precursor = cursor;
|
|
gtk_text_iter_backward_char (&precursor);
|
|
|
|
highlight = gtk_text_iter_has_tag (&cursor, spell->tag_highlight) ||
|
|
gtk_text_iter_has_tag (&precursor, spell->tag_highlight);
|
|
|
|
gtk_text_buffer_remove_tag (GTK_TEXT_BUFFER (spell->doc),
|
|
spell->tag_highlight,
|
|
&start,
|
|
&end);
|
|
|
|
/* Fix a corner case when replacement occurs at beginning of buffer:
|
|
* An iter at offset 0 seems to always be inside a word,
|
|
* even if it's not. Possibly a pango bug.
|
|
*/
|
|
if (gtk_text_iter_get_offset (&start) == 0)
|
|
{
|
|
gtk_text_iter_forward_word_end(&start);
|
|
gtk_text_iter_backward_word_start(&start);
|
|
}
|
|
|
|
wstart = start;
|
|
|
|
while (pluma_spell_utils_skip_no_spell_check (&wstart, &end) &&
|
|
gtk_text_iter_compare (&wstart, &end) < 0)
|
|
{
|
|
gboolean inword;
|
|
|
|
/* move wend to the end of the current word. */
|
|
wend = wstart;
|
|
|
|
gtk_text_iter_forward_word_end (&wend);
|
|
|
|
inword = (gtk_text_iter_compare (&wstart, &cursor) < 0) &&
|
|
(gtk_text_iter_compare (&cursor, &wend) <= 0);
|
|
|
|
if (inword && !force_all)
|
|
{
|
|
/* this word is being actively edited,
|
|
* only check if it's already highligted,
|
|
* otherwise defer this check until later. */
|
|
if (highlight)
|
|
check_word (spell, &wstart, &wend);
|
|
else
|
|
spell->deferred_check = TRUE;
|
|
}
|
|
else
|
|
{
|
|
check_word (spell, &wstart, &wend);
|
|
spell->deferred_check = FALSE;
|
|
}
|
|
|
|
/* now move wend to the beginning of the next word, */
|
|
gtk_text_iter_forward_word_end (&wend);
|
|
gtk_text_iter_backward_word_start (&wend);
|
|
|
|
/* make sure we've actually advanced
|
|
* (we don't advance in some corner cases), */
|
|
if (gtk_text_iter_equal (&wstart, &wend))
|
|
break; /* we're done in these cases.. */
|
|
|
|
/* and then pick this as the new next word beginning. */
|
|
wstart = wend;
|
|
}
|
|
}
|
|
|
|
static void
|
|
check_deferred_range (PlumaAutomaticSpellChecker *spell,
|
|
gboolean force_all)
|
|
{
|
|
GtkTextIter start, end;
|
|
|
|
gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (spell->doc),
|
|
&start,
|
|
spell->mark_insert_start);
|
|
gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (spell->doc),
|
|
&end,
|
|
spell->mark_insert_end);
|
|
|
|
check_range (spell, start, end, force_all);
|
|
}
|
|
|
|
/* insertion works like this:
|
|
* - before the text is inserted, we mark the position in the buffer.
|
|
* - after the text is inserted, we see where our mark is and use that and
|
|
* the current position to check the entire range of inserted text.
|
|
*
|
|
* this may be overkill for the common case (inserting one character). */
|
|
|
|
static void
|
|
insert_text_before (GtkTextBuffer *buffer, GtkTextIter *iter,
|
|
gchar *text, gint len, PlumaAutomaticSpellChecker *spell)
|
|
{
|
|
gtk_text_buffer_move_mark (buffer, spell->mark_insert_start, iter);
|
|
}
|
|
|
|
static void
|
|
insert_text_after (GtkTextBuffer *buffer, GtkTextIter *iter,
|
|
gchar *text, gint len, PlumaAutomaticSpellChecker *spell)
|
|
{
|
|
GtkTextIter start;
|
|
|
|
/* we need to check a range of text. */
|
|
gtk_text_buffer_get_iter_at_mark (buffer, &start, spell->mark_insert_start);
|
|
|
|
check_range (spell, start, *iter, FALSE);
|
|
|
|
gtk_text_buffer_move_mark (buffer, spell->mark_insert_end, iter);
|
|
}
|
|
|
|
/* deleting is more simple: we're given the range of deleted text.
|
|
* after deletion, the start and end iters should be at the same position
|
|
* (because all of the text between them was deleted!).
|
|
* this means we only really check the words immediately bounding the
|
|
* deletion.
|
|
*/
|
|
|
|
static void
|
|
delete_range_after (GtkTextBuffer *buffer, GtkTextIter *start, GtkTextIter *end,
|
|
PlumaAutomaticSpellChecker *spell)
|
|
{
|
|
check_range (spell, *start, *end, FALSE);
|
|
}
|
|
|
|
static void
|
|
mark_set (GtkTextBuffer *buffer,
|
|
GtkTextIter *iter,
|
|
GtkTextMark *mark,
|
|
PlumaAutomaticSpellChecker *spell)
|
|
{
|
|
/* if the cursor has moved and there is a deferred check so handle it now */
|
|
if ((mark == gtk_text_buffer_get_insert (buffer)) && spell->deferred_check)
|
|
check_deferred_range (spell, FALSE);
|
|
}
|
|
|
|
static void
|
|
get_word_extents_from_mark (GtkTextBuffer *buffer,
|
|
GtkTextIter *start,
|
|
GtkTextIter *end,
|
|
GtkTextMark *mark)
|
|
{
|
|
gtk_text_buffer_get_iter_at_mark(buffer, start, mark);
|
|
|
|
if (!gtk_text_iter_starts_word (start))
|
|
gtk_text_iter_backward_word_start (start);
|
|
|
|
*end = *start;
|
|
|
|
if (gtk_text_iter_inside_word (end))
|
|
gtk_text_iter_forward_word_end (end);
|
|
}
|
|
|
|
static void
|
|
remove_tag_to_word (PlumaAutomaticSpellChecker *spell, const gchar *word)
|
|
{
|
|
GtkTextIter iter;
|
|
GtkTextIter match_start, match_end;
|
|
|
|
gboolean found;
|
|
|
|
gtk_text_buffer_get_iter_at_offset (GTK_TEXT_BUFFER (spell->doc), &iter, 0);
|
|
|
|
found = TRUE;
|
|
|
|
while (found)
|
|
{
|
|
found = gtk_text_iter_forward_search (&iter,
|
|
word,
|
|
GTK_TEXT_SEARCH_VISIBLE_ONLY | GTK_TEXT_SEARCH_TEXT_ONLY,
|
|
&match_start,
|
|
&match_end,
|
|
NULL);
|
|
|
|
if (found)
|
|
{
|
|
if (gtk_text_iter_starts_word (&match_start) &&
|
|
gtk_text_iter_ends_word (&match_end))
|
|
{
|
|
gtk_text_buffer_remove_tag (GTK_TEXT_BUFFER (spell->doc),
|
|
spell->tag_highlight,
|
|
&match_start,
|
|
&match_end);
|
|
}
|
|
|
|
iter = match_end;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void
|
|
add_to_dictionary (GtkWidget *menuitem, PlumaAutomaticSpellChecker *spell)
|
|
{
|
|
gchar *word;
|
|
|
|
GtkTextIter start, end;
|
|
|
|
get_word_extents_from_mark (GTK_TEXT_BUFFER (spell->doc), &start, &end, spell->mark_click);
|
|
|
|
word = gtk_text_buffer_get_text (GTK_TEXT_BUFFER (spell->doc),
|
|
&start,
|
|
&end,
|
|
FALSE);
|
|
|
|
pluma_spell_checker_add_word_to_personal (spell->spell_checker, word, -1);
|
|
|
|
g_free (word);
|
|
}
|
|
|
|
static void
|
|
ignore_all (GtkWidget *menuitem, PlumaAutomaticSpellChecker *spell)
|
|
{
|
|
gchar *word;
|
|
|
|
GtkTextIter start, end;
|
|
|
|
get_word_extents_from_mark (GTK_TEXT_BUFFER (spell->doc), &start, &end, spell->mark_click);
|
|
|
|
word = gtk_text_buffer_get_text (GTK_TEXT_BUFFER (spell->doc),
|
|
&start,
|
|
&end,
|
|
FALSE);
|
|
|
|
pluma_spell_checker_add_word_to_session (spell->spell_checker, word, -1);
|
|
|
|
g_free (word);
|
|
}
|
|
|
|
static void
|
|
replace_word (GtkWidget *menuitem, PlumaAutomaticSpellChecker *spell)
|
|
{
|
|
gchar *oldword;
|
|
const gchar *newword;
|
|
|
|
GtkTextIter start, end;
|
|
|
|
get_word_extents_from_mark (GTK_TEXT_BUFFER (spell->doc), &start, &end, spell->mark_click);
|
|
|
|
oldword = gtk_text_buffer_get_text (GTK_TEXT_BUFFER (spell->doc), &start, &end, FALSE);
|
|
|
|
newword = g_object_get_qdata (G_OBJECT (menuitem), suggestion_id);
|
|
g_return_if_fail (newword != NULL);
|
|
|
|
gtk_text_buffer_begin_user_action (GTK_TEXT_BUFFER (spell->doc));
|
|
|
|
gtk_text_buffer_delete (GTK_TEXT_BUFFER (spell->doc), &start, &end);
|
|
gtk_text_buffer_insert (GTK_TEXT_BUFFER (spell->doc), &start, newword, -1);
|
|
|
|
gtk_text_buffer_end_user_action (GTK_TEXT_BUFFER (spell->doc));
|
|
|
|
pluma_spell_checker_set_correction (spell->spell_checker,
|
|
oldword, strlen (oldword),
|
|
newword, strlen (newword));
|
|
|
|
g_free (oldword);
|
|
}
|
|
|
|
static GtkWidget *
|
|
build_suggestion_menu (PlumaAutomaticSpellChecker *spell, const gchar *word)
|
|
{
|
|
GtkWidget *topmenu, *menu;
|
|
GtkWidget *mi;
|
|
GSList *suggestions;
|
|
GSList *list;
|
|
gchar *label_text;
|
|
|
|
topmenu = menu = gtk_menu_new();
|
|
|
|
suggestions = pluma_spell_checker_get_suggestions (spell->spell_checker, word, -1);
|
|
|
|
list = suggestions;
|
|
|
|
if (suggestions == NULL)
|
|
{
|
|
/* no suggestions. put something in the menu anyway... */
|
|
GtkWidget *label;
|
|
/* Translators: Displayed in the "Check Spelling" dialog if there are no suggestions for the current misspelled word */
|
|
label = gtk_label_new (_("(no suggested words)"));
|
|
|
|
mi = gtk_menu_item_new ();
|
|
gtk_widget_set_sensitive (mi, FALSE);
|
|
gtk_container_add (GTK_CONTAINER(mi), label);
|
|
gtk_widget_show_all (mi);
|
|
gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), mi);
|
|
}
|
|
else
|
|
{
|
|
gint count = 0;
|
|
|
|
/* build a set of menus with suggestions. */
|
|
while (suggestions != NULL)
|
|
{
|
|
GtkWidget *label;
|
|
|
|
if (count == 10)
|
|
{
|
|
/* Separator */
|
|
mi = gtk_menu_item_new ();
|
|
gtk_widget_show (mi);
|
|
gtk_menu_shell_append (GTK_MENU_SHELL (menu), mi);
|
|
|
|
mi = gtk_menu_item_new_with_mnemonic (_("_More..."));
|
|
gtk_widget_show (mi);
|
|
gtk_menu_shell_append (GTK_MENU_SHELL (menu), mi);
|
|
|
|
menu = gtk_menu_new ();
|
|
gtk_menu_item_set_submenu (GTK_MENU_ITEM (mi), menu);
|
|
count = 0;
|
|
}
|
|
|
|
label_text = g_strdup_printf ("<b>%s</b>", (gchar*) suggestions->data);
|
|
|
|
label = gtk_label_new (label_text);
|
|
gtk_label_set_use_markup (GTK_LABEL (label), TRUE);
|
|
gtk_misc_set_alignment (GTK_MISC (label), 0.0, 0.5);
|
|
|
|
mi = gtk_menu_item_new ();
|
|
gtk_container_add (GTK_CONTAINER(mi), label);
|
|
|
|
gtk_widget_show_all (mi);
|
|
gtk_menu_shell_append (GTK_MENU_SHELL (menu), mi);
|
|
|
|
g_object_set_qdata_full (G_OBJECT (mi),
|
|
suggestion_id,
|
|
g_strdup (suggestions->data),
|
|
(GDestroyNotify)g_free);
|
|
|
|
g_free (label_text);
|
|
g_signal_connect (mi,
|
|
"activate",
|
|
G_CALLBACK (replace_word),
|
|
spell);
|
|
|
|
count++;
|
|
|
|
suggestions = g_slist_next (suggestions);
|
|
}
|
|
}
|
|
|
|
/* free the suggestion list */
|
|
suggestions = list;
|
|
|
|
while (list)
|
|
{
|
|
g_free (list->data);
|
|
list = g_slist_next (list);
|
|
}
|
|
|
|
g_slist_free (suggestions);
|
|
|
|
/* Separator */
|
|
mi = gtk_menu_item_new ();
|
|
gtk_widget_show (mi);
|
|
gtk_menu_shell_append (GTK_MENU_SHELL (topmenu), mi);
|
|
|
|
/* Ignore all */
|
|
mi = gtk_image_menu_item_new_with_mnemonic (_("_Ignore All"));
|
|
gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (mi),
|
|
gtk_image_new_from_stock (GTK_STOCK_GOTO_BOTTOM,
|
|
GTK_ICON_SIZE_MENU));
|
|
|
|
g_signal_connect (mi,
|
|
"activate",
|
|
G_CALLBACK(ignore_all),
|
|
spell);
|
|
|
|
gtk_widget_show_all (mi);
|
|
|
|
gtk_menu_shell_append (GTK_MENU_SHELL (topmenu), mi);
|
|
|
|
/* + Add to Dictionary */
|
|
mi = gtk_image_menu_item_new_with_mnemonic (_("_Add"));
|
|
gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (mi),
|
|
gtk_image_new_from_stock (GTK_STOCK_ADD,
|
|
GTK_ICON_SIZE_MENU));
|
|
|
|
g_signal_connect (mi,
|
|
"activate",
|
|
G_CALLBACK (add_to_dictionary),
|
|
spell);
|
|
|
|
gtk_widget_show_all (mi);
|
|
|
|
gtk_menu_shell_append (GTK_MENU_SHELL (topmenu), mi);
|
|
|
|
return topmenu;
|
|
}
|
|
|
|
static void
|
|
populate_popup (GtkTextView *textview, GtkMenu *menu, PlumaAutomaticSpellChecker *spell)
|
|
{
|
|
GtkWidget *img, *mi;
|
|
GtkTextIter start, end;
|
|
char *word;
|
|
|
|
/* we need to figure out if they picked a misspelled word. */
|
|
get_word_extents_from_mark (GTK_TEXT_BUFFER (spell->doc), &start, &end, spell->mark_click);
|
|
|
|
/* if our highlight algorithm ever messes up,
|
|
* this isn't correct, either. */
|
|
if (!gtk_text_iter_has_tag (&start, spell->tag_highlight))
|
|
return; /* word wasn't misspelled. */
|
|
|
|
/* menu separator comes first. */
|
|
mi = gtk_menu_item_new ();
|
|
gtk_widget_show (mi);
|
|
gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), mi);
|
|
|
|
/* then, on top of it, the suggestions menu. */
|
|
img = gtk_image_new_from_stock (GTK_STOCK_SPELL_CHECK, GTK_ICON_SIZE_MENU);
|
|
mi = gtk_image_menu_item_new_with_mnemonic (_("_Spelling Suggestions..."));
|
|
gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (mi), img);
|
|
|
|
word = gtk_text_buffer_get_text (GTK_TEXT_BUFFER (spell->doc), &start, &end, FALSE);
|
|
gtk_menu_item_set_submenu (GTK_MENU_ITEM (mi),
|
|
build_suggestion_menu (spell, word));
|
|
g_free(word);
|
|
|
|
gtk_widget_show_all (mi);
|
|
gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), mi);
|
|
}
|
|
|
|
void
|
|
pluma_automatic_spell_checker_recheck_all (PlumaAutomaticSpellChecker *spell)
|
|
{
|
|
GtkTextIter start, end;
|
|
|
|
g_return_if_fail (spell != NULL);
|
|
|
|
gtk_text_buffer_get_bounds (GTK_TEXT_BUFFER (spell->doc), &start, &end);
|
|
|
|
check_range (spell, start, end, TRUE);
|
|
}
|
|
|
|
static void
|
|
add_word_signal_cb (PlumaSpellChecker *checker,
|
|
const gchar *word,
|
|
gint len,
|
|
PlumaAutomaticSpellChecker *spell)
|
|
{
|
|
gchar *w;
|
|
|
|
if (len < 0)
|
|
w = g_strdup (word);
|
|
else
|
|
w = g_strndup (word, len);
|
|
|
|
remove_tag_to_word (spell, w);
|
|
|
|
g_free (w);
|
|
}
|
|
|
|
static void
|
|
set_language_cb (PlumaSpellChecker *checker,
|
|
const PlumaSpellCheckerLanguage *lang,
|
|
PlumaAutomaticSpellChecker *spell)
|
|
{
|
|
pluma_automatic_spell_checker_recheck_all (spell);
|
|
}
|
|
|
|
static void
|
|
clear_session_cb (PlumaSpellChecker *checker,
|
|
PlumaAutomaticSpellChecker *spell)
|
|
{
|
|
pluma_automatic_spell_checker_recheck_all (spell);
|
|
}
|
|
|
|
/* When the user right-clicks on a word, they want to check that word.
|
|
* Here, we do NOT move the cursor to the location of the clicked-upon word
|
|
* since that prevents the use of edit functions on the context menu.
|
|
*/
|
|
static gboolean
|
|
button_press_event (GtkTextView *view,
|
|
GdkEventButton *event,
|
|
PlumaAutomaticSpellChecker *spell)
|
|
{
|
|
if (event->button == 3)
|
|
{
|
|
gint x, y;
|
|
GtkTextIter iter;
|
|
|
|
GtkTextBuffer *buffer = gtk_text_view_get_buffer (view);
|
|
|
|
/* handle deferred check if it exists */
|
|
if (spell->deferred_check)
|
|
check_deferred_range (spell, TRUE);
|
|
|
|
gtk_text_view_window_to_buffer_coords (view,
|
|
GTK_TEXT_WINDOW_TEXT,
|
|
event->x, event->y,
|
|
&x, &y);
|
|
|
|
gtk_text_view_get_iter_at_location (view, &iter, x, y);
|
|
|
|
gtk_text_buffer_move_mark (buffer, spell->mark_click, &iter);
|
|
}
|
|
|
|
return FALSE; /* false: let gtk process this event, too.
|
|
we don't want to eat any events. */
|
|
}
|
|
|
|
/* Move the insert mark before popping up the menu, otherwise it
|
|
* will contain the wrong set of suggestions.
|
|
*/
|
|
static gboolean
|
|
popup_menu_event (GtkTextView *view, PlumaAutomaticSpellChecker *spell)
|
|
{
|
|
GtkTextIter iter;
|
|
GtkTextBuffer *buffer;
|
|
|
|
buffer = gtk_text_view_get_buffer (view);
|
|
|
|
/* handle deferred check if it exists */
|
|
if (spell->deferred_check)
|
|
check_deferred_range (spell, TRUE);
|
|
|
|
gtk_text_buffer_get_iter_at_mark (buffer, &iter,
|
|
gtk_text_buffer_get_insert (buffer));
|
|
gtk_text_buffer_move_mark (buffer, spell->mark_click, &iter);
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
static void
|
|
tag_table_changed (GtkTextTagTable *table,
|
|
PlumaAutomaticSpellChecker *spell)
|
|
{
|
|
g_return_if_fail (spell->tag_highlight != NULL);
|
|
|
|
gtk_text_tag_set_priority (spell->tag_highlight,
|
|
gtk_text_tag_table_get_size (table) - 1);
|
|
}
|
|
|
|
static void
|
|
tag_added_or_removed (GtkTextTagTable *table,
|
|
GtkTextTag *tag,
|
|
PlumaAutomaticSpellChecker *spell)
|
|
{
|
|
tag_table_changed (table, spell);
|
|
}
|
|
|
|
static void
|
|
tag_changed (GtkTextTagTable *table,
|
|
GtkTextTag *tag,
|
|
gboolean size_changed,
|
|
PlumaAutomaticSpellChecker *spell)
|
|
{
|
|
tag_table_changed (table, spell);
|
|
}
|
|
|
|
static void
|
|
highlight_updated (GtkSourceBuffer *buffer,
|
|
GtkTextIter *start,
|
|
GtkTextIter *end,
|
|
PlumaAutomaticSpellChecker *spell)
|
|
{
|
|
check_range (spell, *start, *end, FALSE);
|
|
}
|
|
|
|
static void
|
|
spell_tag_destroyed (PlumaAutomaticSpellChecker *spell,
|
|
GObject *where_the_object_was)
|
|
{
|
|
spell->tag_highlight = NULL;
|
|
}
|
|
|
|
PlumaAutomaticSpellChecker *
|
|
pluma_automatic_spell_checker_new (PlumaDocument *doc,
|
|
PlumaSpellChecker *checker)
|
|
{
|
|
PlumaAutomaticSpellChecker *spell;
|
|
GtkTextTagTable *tag_table;
|
|
GtkTextIter start, end;
|
|
|
|
g_return_val_if_fail (PLUMA_IS_DOCUMENT (doc), NULL);
|
|
g_return_val_if_fail (PLUMA_IS_SPELL_CHECKER (checker), NULL);
|
|
g_return_val_if_fail ((spell = pluma_automatic_spell_checker_get_from_document (doc)) == NULL,
|
|
spell);
|
|
|
|
/* attach to the widget */
|
|
spell = g_new0 (PlumaAutomaticSpellChecker, 1);
|
|
|
|
spell->doc = doc;
|
|
spell->spell_checker = g_object_ref (checker);
|
|
|
|
if (automatic_spell_checker_id == 0)
|
|
{
|
|
automatic_spell_checker_id =
|
|
g_quark_from_string ("PlumaAutomaticSpellCheckerID");
|
|
}
|
|
if (suggestion_id == 0)
|
|
{
|
|
suggestion_id = g_quark_from_string ("PlumaAutoSuggestionID");
|
|
}
|
|
|
|
g_object_set_qdata_full (G_OBJECT (doc),
|
|
automatic_spell_checker_id,
|
|
spell,
|
|
(GDestroyNotify)pluma_automatic_spell_checker_free_internal);
|
|
|
|
g_signal_connect (doc,
|
|
"insert-text",
|
|
G_CALLBACK (insert_text_before),
|
|
spell);
|
|
g_signal_connect_after (doc,
|
|
"insert-text",
|
|
G_CALLBACK (insert_text_after),
|
|
spell);
|
|
g_signal_connect_after (doc,
|
|
"delete-range",
|
|
G_CALLBACK (delete_range_after),
|
|
spell);
|
|
g_signal_connect (doc,
|
|
"mark-set",
|
|
G_CALLBACK (mark_set),
|
|
spell);
|
|
|
|
g_signal_connect (doc,
|
|
"highlight-updated",
|
|
G_CALLBACK (highlight_updated),
|
|
spell);
|
|
|
|
g_signal_connect (spell->spell_checker,
|
|
"add_word_to_session",
|
|
G_CALLBACK (add_word_signal_cb),
|
|
spell);
|
|
g_signal_connect (spell->spell_checker,
|
|
"add_word_to_personal",
|
|
G_CALLBACK (add_word_signal_cb),
|
|
spell);
|
|
g_signal_connect (spell->spell_checker,
|
|
"clear_session",
|
|
G_CALLBACK (clear_session_cb),
|
|
spell);
|
|
g_signal_connect (spell->spell_checker,
|
|
"set_language",
|
|
G_CALLBACK (set_language_cb),
|
|
spell);
|
|
|
|
spell->tag_highlight = gtk_text_buffer_create_tag (
|
|
GTK_TEXT_BUFFER (doc),
|
|
"gtkspell-misspelled",
|
|
"underline", PANGO_UNDERLINE_ERROR,
|
|
NULL);
|
|
|
|
g_object_weak_ref (G_OBJECT (spell->tag_highlight),
|
|
(GWeakNotify)spell_tag_destroyed,
|
|
spell);
|
|
|
|
tag_table = gtk_text_buffer_get_tag_table (GTK_TEXT_BUFFER (doc));
|
|
|
|
gtk_text_tag_set_priority (spell->tag_highlight,
|
|
gtk_text_tag_table_get_size (tag_table) - 1);
|
|
|
|
g_signal_connect (tag_table,
|
|
"tag-added",
|
|
G_CALLBACK (tag_added_or_removed),
|
|
spell);
|
|
g_signal_connect (tag_table,
|
|
"tag-removed",
|
|
G_CALLBACK (tag_added_or_removed),
|
|
spell);
|
|
g_signal_connect (tag_table,
|
|
"tag-changed",
|
|
G_CALLBACK (tag_changed),
|
|
spell);
|
|
|
|
/* we create the mark here, but we don't use it until text is
|
|
* inserted, so we don't really care where iter points. */
|
|
gtk_text_buffer_get_bounds (GTK_TEXT_BUFFER (doc), &start, &end);
|
|
|
|
spell->mark_insert_start = gtk_text_buffer_get_mark (GTK_TEXT_BUFFER (doc),
|
|
"pluma-automatic-spell-checker-insert-start");
|
|
|
|
if (spell->mark_insert_start == NULL)
|
|
{
|
|
spell->mark_insert_start =
|
|
gtk_text_buffer_create_mark (GTK_TEXT_BUFFER (doc),
|
|
"pluma-automatic-spell-checker-insert-start",
|
|
&start,
|
|
TRUE);
|
|
}
|
|
else
|
|
{
|
|
gtk_text_buffer_move_mark (GTK_TEXT_BUFFER (doc),
|
|
spell->mark_insert_start,
|
|
&start);
|
|
}
|
|
|
|
spell->mark_insert_end = gtk_text_buffer_get_mark (GTK_TEXT_BUFFER (doc),
|
|
"pluma-automatic-spell-checker-insert-end");
|
|
|
|
if (spell->mark_insert_end == NULL)
|
|
{
|
|
spell->mark_insert_end =
|
|
gtk_text_buffer_create_mark (GTK_TEXT_BUFFER (doc),
|
|
"pluma-automatic-spell-checker-insert-end",
|
|
&start,
|
|
TRUE);
|
|
}
|
|
else
|
|
{
|
|
gtk_text_buffer_move_mark (GTK_TEXT_BUFFER (doc),
|
|
spell->mark_insert_end,
|
|
&start);
|
|
}
|
|
|
|
spell->mark_click = gtk_text_buffer_get_mark (GTK_TEXT_BUFFER (doc),
|
|
"pluma-automatic-spell-checker-click");
|
|
|
|
if (spell->mark_click == NULL)
|
|
{
|
|
spell->mark_click =
|
|
gtk_text_buffer_create_mark (GTK_TEXT_BUFFER (doc),
|
|
"pluma-automatic-spell-checker-click",
|
|
&start,
|
|
TRUE);
|
|
}
|
|
else
|
|
{
|
|
gtk_text_buffer_move_mark (GTK_TEXT_BUFFER (doc),
|
|
spell->mark_click,
|
|
&start);
|
|
}
|
|
|
|
spell->deferred_check = FALSE;
|
|
|
|
return spell;
|
|
}
|
|
|
|
PlumaAutomaticSpellChecker *
|
|
pluma_automatic_spell_checker_get_from_document (const PlumaDocument *doc)
|
|
{
|
|
g_return_val_if_fail (PLUMA_IS_DOCUMENT (doc), NULL);
|
|
|
|
if (automatic_spell_checker_id == 0)
|
|
return NULL;
|
|
|
|
return g_object_get_qdata (G_OBJECT (doc), automatic_spell_checker_id);
|
|
}
|
|
|
|
void
|
|
pluma_automatic_spell_checker_free (PlumaAutomaticSpellChecker *spell)
|
|
{
|
|
g_return_if_fail (spell != NULL);
|
|
g_return_if_fail (pluma_automatic_spell_checker_get_from_document (spell->doc) == spell);
|
|
|
|
if (automatic_spell_checker_id == 0)
|
|
return;
|
|
|
|
g_object_set_qdata (G_OBJECT (spell->doc), automatic_spell_checker_id, NULL);
|
|
}
|
|
|
|
static void
|
|
pluma_automatic_spell_checker_free_internal (PlumaAutomaticSpellChecker *spell)
|
|
{
|
|
GtkTextTagTable *table;
|
|
GtkTextIter start, end;
|
|
GSList *list;
|
|
|
|
g_return_if_fail (spell != NULL);
|
|
|
|
table = gtk_text_buffer_get_tag_table (GTK_TEXT_BUFFER (spell->doc));
|
|
|
|
if (table != NULL && spell->tag_highlight != NULL)
|
|
{
|
|
gtk_text_buffer_get_bounds (GTK_TEXT_BUFFER (spell->doc),
|
|
&start,
|
|
&end);
|
|
gtk_text_buffer_remove_tag (GTK_TEXT_BUFFER (spell->doc),
|
|
spell->tag_highlight,
|
|
&start,
|
|
&end);
|
|
|
|
g_signal_handlers_disconnect_matched (G_OBJECT (table),
|
|
G_SIGNAL_MATCH_DATA,
|
|
0, 0, NULL, NULL,
|
|
spell);
|
|
|
|
gtk_text_tag_table_remove (table, spell->tag_highlight);
|
|
}
|
|
|
|
g_signal_handlers_disconnect_matched (G_OBJECT (spell->doc),
|
|
G_SIGNAL_MATCH_DATA,
|
|
0, 0, NULL, NULL,
|
|
spell);
|
|
|
|
g_signal_handlers_disconnect_matched (G_OBJECT (spell->spell_checker),
|
|
G_SIGNAL_MATCH_DATA,
|
|
0, 0, NULL, NULL,
|
|
spell);
|
|
|
|
g_object_unref (spell->spell_checker);
|
|
|
|
list = spell->views;
|
|
while (list != NULL)
|
|
{
|
|
PlumaView *view = PLUMA_VIEW (list->data);
|
|
|
|
g_signal_handlers_disconnect_matched (G_OBJECT (view),
|
|
G_SIGNAL_MATCH_DATA,
|
|
0, 0, NULL, NULL,
|
|
spell);
|
|
|
|
g_signal_handlers_disconnect_matched (G_OBJECT (view),
|
|
G_SIGNAL_MATCH_DATA,
|
|
0, 0, NULL, NULL,
|
|
spell);
|
|
|
|
list = g_slist_next (list);
|
|
}
|
|
|
|
g_slist_free (spell->views);
|
|
|
|
g_free (spell);
|
|
}
|
|
|
|
void
|
|
pluma_automatic_spell_checker_attach_view (
|
|
PlumaAutomaticSpellChecker *spell,
|
|
PlumaView *view)
|
|
{
|
|
g_return_if_fail (spell != NULL);
|
|
g_return_if_fail (PLUMA_IS_VIEW (view));
|
|
|
|
g_return_if_fail (gtk_text_view_get_buffer (GTK_TEXT_VIEW (view)) ==
|
|
GTK_TEXT_BUFFER (spell->doc));
|
|
|
|
g_signal_connect (view,
|
|
"button-press-event",
|
|
G_CALLBACK (button_press_event),
|
|
spell);
|
|
g_signal_connect (view,
|
|
"popup-menu",
|
|
G_CALLBACK (popup_menu_event),
|
|
spell);
|
|
g_signal_connect (view,
|
|
"populate-popup",
|
|
G_CALLBACK (populate_popup),
|
|
spell);
|
|
g_signal_connect (view,
|
|
"destroy",
|
|
G_CALLBACK (view_destroy),
|
|
spell);
|
|
|
|
spell->views = g_slist_prepend (spell->views, view);
|
|
}
|
|
|
|
void
|
|
pluma_automatic_spell_checker_detach_view (
|
|
PlumaAutomaticSpellChecker *spell,
|
|
PlumaView *view)
|
|
{
|
|
g_return_if_fail (spell != NULL);
|
|
g_return_if_fail (PLUMA_IS_VIEW (view));
|
|
|
|
g_return_if_fail (gtk_text_view_get_buffer (GTK_TEXT_VIEW (view)) ==
|
|
GTK_TEXT_BUFFER (spell->doc));
|
|
g_return_if_fail (spell->views != NULL);
|
|
|
|
g_signal_handlers_disconnect_matched (G_OBJECT (view),
|
|
G_SIGNAL_MATCH_DATA,
|
|
0, 0, NULL, NULL,
|
|
spell);
|
|
|
|
g_signal_handlers_disconnect_matched (G_OBJECT (view),
|
|
G_SIGNAL_MATCH_DATA,
|
|
0, 0, NULL, NULL,
|
|
spell);
|
|
|
|
spell->views = g_slist_remove (spell->views, view);
|
|
}
|
|
|