Blob Blame History Raw
/* gtkspell - a spell-checking addon for GTK's TextView widget
 * Copyright (c) 2002 Evan Martin.
 */

/* vim: set ts=4 sw=4 wm=5 : */

#include <string.h>
#include <gtk/gtk.h>
#include <libintl.h>
#include <locale.h>
#include "../config.h"
#include "gtkspell.h"

#define _(String) dgettext (PACKAGE, String)

#define GTKSPELL_MISSPELLED_TAG "gtkspell-misspelled"

#include <enchant.h>

static const int debug = 0;
static const int quiet = 0;

static EnchantBroker *broker = NULL;
static int broker_ref_cnt;

struct _GtkSpell {
	GtkTextView *view;
	GtkTextBuffer *buffer;
	GtkTextTag *tag_highlight;
	GtkTextMark *mark_insert_start;
	GtkTextMark *mark_insert_end;
	gboolean deferred_check;
	EnchantDict *speller;
	GtkTextMark *mark_click;
	gchar *lang;
};

static void gtkspell_free(GtkSpell *spell);

#define GTKSPELL_OBJECT_KEY "gtkspell"

GQuark
gtkspell_error_quark(void) {
	static GQuark q = 0;
	if (q == 0)
		q = g_quark_from_static_string("gtkspell-error-quark");
	return q;
}

static gboolean
gtkspell_text_iter_forward_word_end(GtkTextIter *i) {
	GtkTextIter iter;

/* heuristic: 
 * if we're on an singlequote/apostrophe and
 * if the next letter is alphanumeric,
 * this is an apostrophe. */

	if (!gtk_text_iter_forward_word_end(i))
		return FALSE;

	if (gtk_text_iter_get_char(i) != '\'')
		return TRUE;

	iter = *i;
	if (gtk_text_iter_forward_char(&iter)) {
		if (g_unichar_isalpha(gtk_text_iter_get_char(&iter))) {
			return (gtk_text_iter_forward_word_end(i));
		}
	}

	return TRUE;
}

static gboolean
gtkspell_text_iter_backward_word_start(GtkTextIter *i) {
	GtkTextIter iter;

	if (!gtk_text_iter_backward_word_start(i))
		return FALSE;

	iter = *i;
	if (gtk_text_iter_backward_char(&iter)) {
		if (gtk_text_iter_get_char(&iter) == '\'') {
			if (gtk_text_iter_backward_char(&iter)) {
				if (g_unichar_isalpha(gtk_text_iter_get_char(&iter))) {
					return (gtk_text_iter_backward_word_start(i));
				}
			}
		}
	}

	return TRUE;
}

#define gtk_text_iter_backward_word_start gtkspell_text_iter_backward_word_start
#define gtk_text_iter_forward_word_end gtkspell_text_iter_forward_word_end

static void
check_word(GtkSpell *spell, GtkTextBuffer *buffer,
           GtkTextIter *start, GtkTextIter *end) {
	char *text;
	if (!spell->speller)
		return;
	text = gtk_text_buffer_get_text(buffer, start, end, FALSE);
	if (debug) g_print("checking: %s\n", text);
	if (g_unichar_isdigit(*text) == FALSE) /* don't check numbers */
		if (enchant_dict_check(spell->speller, text, strlen(text)) != 0)
			gtk_text_buffer_apply_tag(buffer, spell->tag_highlight, start, end);
	g_free(text);
}

static void
print_iter(char *name, GtkTextIter *iter) {
	g_print("%1s[%d%c%c%c] ", name, gtk_text_iter_get_offset(iter),
		gtk_text_iter_starts_word(iter) ? 's' : ' ',
		gtk_text_iter_inside_word(iter) ? 'i' : ' ',
		gtk_text_iter_ends_word(iter) ? 'e' : ' ');
}

static void
check_range(GtkSpell *spell, GtkTextBuffer *buffer,
            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, wend, cursor, precursor;
	gboolean inword, highlight;
	if (debug) {
		g_print("check_range: "); print_iter("s", &start); print_iter("e", &end); g_print(" -> ");
	}

	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. */
			//gtk_text_buffer_remove_tag(buffer, tag_highlight, &start, &end);
			if (gtk_text_iter_forward_word_end(&start))
				gtk_text_iter_backward_word_start(&start);
		}
	}
	gtk_text_buffer_get_iter_at_mark(buffer, &cursor,
			gtk_text_buffer_get_insert(buffer));

	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(buffer, 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);
	}

	if (debug) {print_iter("s", &start); print_iter("e", &end); g_print("\n");}

	wstart = start;
	while (gtk_text_iter_compare(&wstart, &end) < 0) {
		/* 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, buffer, &wstart, &wend);
			else
				spell->deferred_check = TRUE;
		} else {
			check_word(spell, buffer, &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(GtkSpell *spell, GtkTextBuffer *buffer, gboolean force_all) {
	GtkTextIter start, end;
	gtk_text_buffer_get_iter_at_mark(buffer, &start, spell->mark_insert_start);
	gtk_text_buffer_get_iter_at_mark(buffer, &end, spell->mark_insert_end);
	check_range(spell, buffer, 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, GtkSpell *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, GtkSpell *spell) {
	GtkTextIter start;

	if (debug) g_print("insert\n");

	/* we need to check a range of text. */
	gtk_text_buffer_get_iter_at_mark(buffer, &start, spell->mark_insert_start);
	check_range(spell, buffer, 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, GtkSpell *spell) {
	if (debug) g_print("delete\n");
	check_range(spell, buffer, *start, *end, FALSE);
}

static void
mark_set(GtkTextBuffer *buffer, GtkTextIter *iter, 
		 GtkTextMark *mark, GtkSpell *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, buffer, 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
add_to_dictionary(GtkWidget *menuitem, GtkSpell *spell) {
	char *word;
	GtkTextIter start, end;

	get_word_extents_from_mark(spell->buffer, &start, &end, spell->mark_click);
	word = gtk_text_buffer_get_text(spell->buffer, &start, &end, FALSE);
	
	enchant_dict_add_to_pwl( spell->speller, word, strlen(word));

	gtkspell_recheck_all(spell);

	g_free(word);
}

static void
ignore_all(GtkWidget *menuitem, GtkSpell *spell) {
	char *word;
	GtkTextIter start, end;

	get_word_extents_from_mark(spell->buffer, &start, &end, spell->mark_click);
	word = gtk_text_buffer_get_text(spell->buffer, &start, &end, FALSE);
	
	enchant_dict_add_to_session(spell->speller, word, strlen(word));

	gtkspell_recheck_all(spell);

	g_free(word);
}

static void
replace_word(GtkWidget *menuitem, GtkSpell *spell) {
	char *oldword;
	const char *newword;
	GtkTextIter start, end;
	
	if (!spell->speller)
		return;

	get_word_extents_from_mark(spell->buffer, &start, &end, spell->mark_click);
	oldword = gtk_text_buffer_get_text(spell->buffer, &start, &end, FALSE);
	newword = gtk_label_get_text(GTK_LABEL(GTK_BIN(menuitem)->child));

	if (debug) {
		g_print("old word: '%s'\n", oldword);
		print_iter("s", &start); print_iter("e", &end);
		g_print("\nnew word: '%s'\n", newword);
	}

	gtk_text_buffer_begin_user_action(spell->buffer);
	gtk_text_buffer_delete(spell->buffer, &start, &end);
	gtk_text_buffer_insert(spell->buffer, &start, newword, -1);
	gtk_text_buffer_end_user_action(spell->buffer);

	enchant_dict_store_replacement(spell->speller, 
			oldword, strlen(oldword),
			newword, strlen(newword));

	g_free(oldword);
}

/* This function populates suggestions at the top of the passed menu */
static void
add_suggestion_menus(GtkSpell *spell, GtkTextBuffer *buffer,
                      const char *word, GtkWidget *topmenu) {
	GtkWidget *menu;
	GtkWidget *mi;
	char **suggestions;
	size_t n_suggs, i;
	char *label;
	
	menu = topmenu;

	if (!spell->speller)
		return;

	gint menu_position = 0;

	suggestions = enchant_dict_suggest(spell->speller, word, strlen(word), &n_suggs);

	if (suggestions == NULL || !n_suggs) {
		/* no suggestions.  put something in the menu anyway... */
		GtkWidget *label;
		label = gtk_label_new("");
		gtk_label_set_markup(GTK_LABEL(label), _("<i>(no suggestions)</i>"));

		mi = gtk_menu_item_new();
		gtk_container_add(GTK_CONTAINER(mi), label);
		gtk_widget_show_all(mi);
		gtk_menu_shell_insert(GTK_MENU_SHELL(menu), mi, menu_position++);
	} else {
		/* build a set of menus with suggestions. */
		gboolean inside_more_submenu = FALSE;
		for (i = 0; i < n_suggs; i++ ) {
			if (i > 0 && i % 10 == 0) {
				inside_more_submenu = TRUE;
				mi = gtk_menu_item_new_with_label(_("More..."));
				gtk_widget_show(mi);
				gtk_menu_shell_insert(GTK_MENU_SHELL(menu), mi, menu_position++);

				menu = gtk_menu_new();
				gtk_menu_item_set_submenu(GTK_MENU_ITEM(mi), menu);
			}
			mi = gtk_menu_item_new_with_label(suggestions[i]);
			g_signal_connect(G_OBJECT(mi), "activate",
					G_CALLBACK(replace_word), spell);
			gtk_widget_show(mi);
			if (inside_more_submenu) gtk_menu_shell_append(GTK_MENU_SHELL(menu), mi);
			else gtk_menu_shell_insert(GTK_MENU_SHELL(menu), mi, menu_position++);
		}
	}

	if (suggestions)
		enchant_dict_free_string_list(spell->speller, suggestions);

	/* + Add to Dictionary */
	label = g_strdup_printf(_("Add \"%s\" to Dictionary"), word);
	mi = gtk_image_menu_item_new_with_label(label);
	g_free(label);
	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(G_OBJECT(mi), "activate",
			G_CALLBACK(add_to_dictionary), spell);
	gtk_widget_show_all(mi);
	gtk_menu_shell_insert(GTK_MENU_SHELL(topmenu), mi, menu_position++);

	/* - Ignore All */
	mi = gtk_image_menu_item_new_with_label(_("Ignore All"));
	gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(mi), 
			gtk_image_new_from_stock(GTK_STOCK_REMOVE, GTK_ICON_SIZE_MENU));
	g_signal_connect(G_OBJECT(mi), "activate",
			G_CALLBACK(ignore_all), spell);
	gtk_widget_show_all(mi);
	gtk_menu_shell_insert(GTK_MENU_SHELL(topmenu), mi, menu_position++);
}

static GtkWidget*
build_suggestion_menu(GtkSpell *spell, GtkTextBuffer *buffer,
                      const char *word) {
	GtkWidget *topmenu;
	topmenu = gtk_menu_new();
	add_suggestion_menus(spell, buffer, word, topmenu);

	return topmenu;
}

static void
language_change_callback(GtkCheckMenuItem *mi, GtkSpell* spell) {
	if (gtk_check_menu_item_get_active(mi)) {
		GError* error = NULL;
		gchar *name;
		g_object_get(G_OBJECT(mi), "name", &name, NULL);
		gtkspell_set_language(spell, name, &error);
		g_free(name);
	}
}

struct _languages_cb_struct {GList *langs;};

static void
dict_describe_cb(const char * const lang_tag,
		 const char * const provider_name,
		 const char * const provider_desc,
		 const char * const provider_file,
		 void * user_data) {

	struct _languages_cb_struct *languages_cb_struct = (struct _languages_cb_struct *)user_data;

	languages_cb_struct->langs = g_list_insert_sorted(
		languages_cb_struct->langs, g_strdup(lang_tag),
		(GCompareFunc) strcmp);
}

static GtkWidget*
build_languages_menu(GtkSpell *spell) {
	GtkWidget *active_item = NULL, *menu = gtk_menu_new();
	GList *langs;
	GSList *menu_group = NULL;

	struct _languages_cb_struct languages_cb_struct;
	languages_cb_struct.langs = NULL;

	enchant_broker_list_dicts(broker, dict_describe_cb, &languages_cb_struct);

	langs = languages_cb_struct.langs;

	for (; langs; langs = langs->next) {
		gchar *lang_tag = langs->data;
		GtkWidget* mi = gtk_radio_menu_item_new_with_label(menu_group, lang_tag);
		menu_group = gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(mi));

		g_object_set(G_OBJECT(mi), "name", lang_tag, NULL);
		if (strcmp(spell->lang, lang_tag) == 0)
			active_item = mi;
		else
			g_signal_connect(G_OBJECT(mi), "activate",
				G_CALLBACK(language_change_callback), spell);
		gtk_widget_show(mi);
		gtk_menu_shell_append(GTK_MENU_SHELL(menu), mi);

		g_free(lang_tag);
	}
	if (active_item)
		gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(active_item), TRUE);

	g_list_free(languages_cb_struct.langs);

	return menu;
}

static void
populate_popup(GtkTextView *textview, GtkMenu *menu, GtkSpell *spell) {
	GtkWidget *mi;
	GtkTextIter start, end;
	char *word;

	/* menu separator comes first. */
	mi = gtk_separator_menu_item_new();
	gtk_widget_show(mi);
	gtk_menu_shell_prepend(GTK_MENU_SHELL(menu), mi);

	/* on top: language selection */
	mi = gtk_menu_item_new_with_label(_("Languages"));
	gtk_menu_item_set_submenu(GTK_MENU_ITEM(mi), build_languages_menu(spell));
	gtk_widget_show_all(mi);
	gtk_menu_shell_prepend(GTK_MENU_SHELL(menu), mi);

	/* we need to figure out if they picked a misspelled word. */
	get_word_extents_from_mark(spell->buffer, &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. */

	/* then, on top of it, the suggestions */
	word = gtk_text_buffer_get_text(spell->buffer, &start, &end, FALSE);
	add_suggestion_menus(spell, spell->buffer, word, GTK_WIDGET (menu) );
	g_free(word);
}

/* 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, GtkSpell *spell) {
	if (event->button == 3) {
		gint x, y;
		GtkTextIter iter;

		/* handle deferred check if it exists */
		if (spell->deferred_check)
			check_deferred_range(spell, spell->buffer, 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(spell->buffer, spell->mark_click, &iter);
	}
	return FALSE; /* false: let gtk process this event, too.
					 we don't want to eat any events. */
}

/* This event occurs when the popup menu is requested through a key-binding
 * (Menu Key or <shift>+F10 by default).  In this case we want to set
 * spell->mark_click to the cursor position. */
static gboolean
popup_menu_event(GtkTextView *view, GtkSpell *spell) {
	GtkTextIter iter;

	gtk_text_buffer_get_iter_at_mark(spell->buffer, &iter, 
			gtk_text_buffer_get_insert(spell->buffer));
	gtk_text_buffer_move_mark(spell->buffer, spell->mark_click, &iter);
	return FALSE; /* false: let gtk process this event, too. */
}

static void
_set_lang_from_dict(const char * const lang_tag,
		    const char * const provider_name,
		    const char * const provider_desc,
		    const char * const provider_dll_file,
		    void * user_data)
{
	GtkSpell *spell = user_data;

	g_free(spell->lang);
	spell->lang = g_strdup(lang_tag);
}

static gboolean
gtkspell_set_language_internal(GtkSpell *spell, const gchar *lang, GError **error) {
	EnchantDict *dict;

	if (lang == NULL) {
		lang = g_getenv("LANG");
		if (lang) {
			if ((strcmp(lang, "C") == 0) || (strcmp(lang, "c") == 0))
				lang = NULL;
			else if (lang[0] == 0)
				lang = NULL;
		}
	}

	if (!lang)
		lang = "en";

	dict = enchant_broker_request_dict(broker, lang);

	if (!dict) {
		g_set_error(error, GTKSPELL_ERROR, GTKSPELL_ERROR_BACKEND,
			_("enchant error for language: %s"), lang);
		return FALSE;
	}

	if (spell->speller)
		enchant_broker_free_dict(broker, spell->speller);
	spell->speller = dict;

	enchant_dict_describe(dict, _set_lang_from_dict, spell);

	return TRUE;
}

/**
 * gtkspell_set_language:
 * @spell:  The #GtkSpell object.
 * @lang: The language to use, in a form enchant understands (it appears to
 * be a locale specifier?).
 * @error: Return location for error.
 *
 * Set the language on @spell to @lang, possibily returning an error in
 * @error.
 *
 * Returns: FALSE if there was an error.
 */
gboolean
gtkspell_set_language(GtkSpell *spell, const gchar *lang, GError **error) {
	gboolean ret;

	if (error)
		g_return_val_if_fail(*error == NULL, FALSE);

	ret = gtkspell_set_language_internal(spell, lang, error);
	if (ret)
		gtkspell_recheck_all(spell);

	return ret;
}

/**
 * gtkspell_recheck_all:
 * @spell:  The #GtkSpell object.
 *
 * Recheck the spelling in the entire buffer.
 */
void
gtkspell_recheck_all(GtkSpell *spell) {
	GtkTextIter start, end;

	gtk_text_buffer_get_bounds(spell->buffer, &start, &end);

	check_range(spell, spell->buffer, start, end, TRUE);
}

/* changes the buffer
 * a NULL buffer is acceptable and will only release the current one */
static void
gtkspell_set_buffer(GtkSpell *spell, GtkTextBuffer *buffer)
{
	GtkTextTagTable *tagtable;
	GtkTextIter start, end;

	if (spell->buffer) {
		g_signal_handlers_disconnect_matched(spell->buffer,
				G_SIGNAL_MATCH_DATA,
				0, 0, NULL, NULL,
				spell);

		tagtable = gtk_text_buffer_get_tag_table(spell->buffer);

		gtk_text_buffer_get_bounds(spell->buffer, &start, &end);
		gtk_text_buffer_remove_tag(spell->buffer, spell->tag_highlight, &start, &end);
		spell->tag_highlight = NULL;

		gtk_text_buffer_delete_mark(spell->buffer, spell->mark_insert_start);
		spell->mark_insert_start = NULL;
		gtk_text_buffer_delete_mark(spell->buffer, spell->mark_insert_end);
		spell->mark_insert_end = NULL;
		gtk_text_buffer_delete_mark(spell->buffer, spell->mark_click);
		spell->mark_click = NULL;

		g_object_unref (spell->buffer);
	}

	spell->buffer = buffer;

	if (spell->buffer) {
		g_object_ref (spell->buffer);

		g_signal_connect(G_OBJECT(spell->buffer),
				"insert-text",
				G_CALLBACK(insert_text_before), spell);
		g_signal_connect_after(G_OBJECT(spell->buffer),
				"insert-text",
				G_CALLBACK(insert_text_after), spell);
		g_signal_connect_after(G_OBJECT(spell->buffer),
				"delete-range",
				G_CALLBACK(delete_range_after), spell);
		g_signal_connect(G_OBJECT(spell->buffer),
				"mark-set",
				G_CALLBACK(mark_set), spell);

		tagtable = gtk_text_buffer_get_tag_table(spell->buffer);
		spell->tag_highlight = gtk_text_tag_table_lookup(tagtable, GTKSPELL_MISSPELLED_TAG);

		if (spell->tag_highlight == NULL) {
			spell->tag_highlight = gtk_text_buffer_create_tag(spell->buffer,
					GTKSPELL_MISSPELLED_TAG,
#ifdef HAVE_PANGO_UNDERLINE_ERROR
					"underline", PANGO_UNDERLINE_ERROR,
#else
					"foreground", "red", 
					"underline", PANGO_UNDERLINE_SINGLE,
#endif
					NULL);
		}

		/* 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(spell->buffer, &start, &end);
		spell->mark_insert_start = gtk_text_buffer_create_mark(spell->buffer,
				"gtkspell-insert-start",
				&start, TRUE);
		spell->mark_insert_end = gtk_text_buffer_create_mark(spell->buffer,
				"gtkspell-insert-end",
				&start, TRUE);
		spell->mark_click = gtk_text_buffer_create_mark(spell->buffer,
				"gtkspell-click",
				&start, TRUE);
			
		spell->deferred_check = FALSE;

		/* now check the entire text buffer. */
		gtkspell_recheck_all(spell);
	}
}

static void
buffer_changed (GtkTextView *view, GParamSpec *pspec, GtkSpell *spell)
{
	gtkspell_set_buffer(spell, gtk_text_view_get_buffer(view));
}

/**
 * gtkspell_new_attach:
 * @view: The #GtkTextView to attach to.
 * @lang: The language to use, in a form pspell understands (it appears to
 * be a locale specifier?).
 * @error: Return location for error.
 *
 * Create a new #GtkSpell object attached to @view with language @lang.
 *
 * Returns: a new #GtkSpell object, or %NULL on error.
 */
GtkSpell*
gtkspell_new_attach(GtkTextView *view, const gchar *lang, GError **error) {
	GtkSpell *spell;

#ifdef ENABLE_NLS
	bindtextdomain(PACKAGE, LOCALEDIR);
	bind_textdomain_codeset(PACKAGE, "UTF-8");
#endif

	if (error)
		g_return_val_if_fail(*error == NULL, NULL);

	spell = g_object_get_data(G_OBJECT(view), GTKSPELL_OBJECT_KEY);
	g_assert(spell == NULL);

	/* We don't need to worry about thread safety.
	 * Stuff shouldn't be attaching to a GtkTextView from anything other
	 * than the mainloop thread */
	if (!broker) {
		broker = enchant_broker_init();
		broker_ref_cnt = 0;
	}
	broker_ref_cnt++;


	/* attach to the widget */
	spell = g_new0(GtkSpell, 1);
	spell->view = view;
	if (!gtkspell_set_language_internal(spell, lang, error)) {
		broker_ref_cnt--;
		if (broker_ref_cnt == 0) {
			enchant_broker_free(broker);
			broker = NULL;
		}
		g_free(spell);
		return NULL;
	}
	g_object_set_data(G_OBJECT(view), GTKSPELL_OBJECT_KEY, spell);

	g_signal_connect_swapped(G_OBJECT(view), "destroy",
			G_CALLBACK(gtkspell_free), spell);
	g_signal_connect(G_OBJECT(view), "button-press-event",
			G_CALLBACK(button_press_event), spell);
	g_signal_connect(G_OBJECT(view), "populate-popup",
			G_CALLBACK(populate_popup), spell);
	g_signal_connect(G_OBJECT(view), "popup-menu",
			G_CALLBACK(popup_menu_event), spell);
	g_signal_connect(G_OBJECT(view), "notify::buffer",
			G_CALLBACK(buffer_changed), spell);

	spell->buffer = NULL;
	gtkspell_set_buffer(spell, gtk_text_view_get_buffer(view));

	return spell;
}

static void
gtkspell_free(GtkSpell *spell) {

	gtkspell_set_buffer(spell, NULL);

	if (broker) {
		if (spell->speller) {
			enchant_broker_free_dict(broker, spell->speller);
		}
		broker_ref_cnt--;
		if (broker_ref_cnt == 0) {
			enchant_broker_free(broker);
			broker = NULL;
		}
	}
	g_signal_handlers_disconnect_matched(spell->view,
			G_SIGNAL_MATCH_DATA,
			0, 0, NULL, NULL,
			spell);
	g_free(spell->lang);
	g_free(spell);
}

/**
 * gtkspell_get_from_text_view:
 * @view: A #GtkTextView.
 *
 * Retrieves the #GtkSpell object attached to a text view.
 *
 * Returns: the #GtkSpell object, or %NULL if there is no #GtkSpell
 * attached to @view.
 */
GtkSpell*
gtkspell_get_from_text_view(GtkTextView *view) {
	return g_object_get_data(G_OBJECT(view), GTKSPELL_OBJECT_KEY);
}

/**
 * gtkspell_detach:
 * @spell: A #GtkSpell.
 *
 * Detaches this #GtkSpell from its text view.  Use
 * gtkspell_get_from_text_view() to retrieve a GtkSpell from a
 * #GtkTextView.
 */
void
gtkspell_detach(GtkSpell *spell) {
	g_return_if_fail(spell != NULL);

	g_object_set_data(G_OBJECT(spell->view), GTKSPELL_OBJECT_KEY, NULL);
	gtkspell_free(spell);
}

/**
 * gtkspell_get_suggestions_menu:
 * @iter: Textiter of position in buffer to be corrected if necessary.
 *
 * Retrieves a submenu of replacement spellings, or NULL if the word at @iter is
 * not misspelt.
 *
 * Returns: the #GtkMenu widget, or %NULL if there is no need for a menu
 */
GtkWidget*
gtkspell_get_suggestions_menu(GtkSpell *spell, GtkTextIter *iter) {
	GtkWidget *submenu = NULL;
	GtkTextIter start, end;

	g_return_val_if_fail(spell != NULL, NULL);

	/* avoid an empty submenu when enchant is not working properly */
	if (!spell->speller)
		return NULL;

	start = *iter;
	/* use the same lazy test, with same risk, as does the default menu arrangement */
	if (gtk_text_iter_has_tag(&start, spell->tag_highlight)) {
		/* word was mis-spelt */
		gchar *badword;
		/* in case a fix is requested, move the attention-point */
		gtk_text_buffer_move_mark(spell->buffer, spell->mark_click, iter);
		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);
		badword = gtk_text_buffer_get_text (spell->buffer, &start, &end, FALSE);

		submenu = build_suggestion_menu (spell, spell->buffer, badword);
		gtk_widget_show (submenu);

		g_free (badword);
	}
	return submenu;
}