/* gtkspell - a spell-checking addon for GTK's TextView widget * Copyright (c) 2002 Evan Martin * Copyright (c) 2012-2013 Sandro Mani * * 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., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ /* vim: set ts=4 sw=4 wm=5 : */ #include "../config.h" #include "gtkspell.h" #include #include #include #include #ifdef HAVE_ISO_CODES #include "gtkspell-codetable.h" #endif #ifdef G_OS_WIN32 #include "gtkspell-win32.h" #endif #define _(String) dgettext (GETTEXT_PACKAGE, String) #define GTK_SPELL_MISSPELLED_TAG "gtkspell-misspelled" #define GTK_SPELL_OBJECT_KEY "gtkspell" static const int debug = 0; static const int quiet = 0; static EnchantBroker *broker = NULL; static int broker_ref_cnt = 0; #ifdef HAVE_ISO_CODES static int codetable_ref_cnt = 0; #endif static void gtk_spell_checker_dispose (GObject *object); static void gtk_spell_checker_finalize (GObject *object); enum { LANGUAGE_CHANGED, LAST_SIGNAL }; static guint signals[LAST_SIGNAL] = { 0 }; enum { PROP_0, PROP_DECODE_LANGUAGE_CODES }; #define GTK_SPELL_CHECKER_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), GTK_SPELL_TYPE_CHECKER, GtkSpellCheckerPrivate)) struct _GtkSpellCheckerPrivate { GtkTextView *view; GtkTextBuffer *buffer; GtkTextTag *tag_highlight; GtkTextMark *mark_insert_start; GtkTextMark *mark_insert_end; GtkTextMark *mark_click; gboolean deferred_check; EnchantDict *speller; gchar *lang; gboolean decode_codes; }; G_DEFINE_TYPE (GtkSpellChecker, gtk_spell_checker, G_TYPE_INITIALLY_UNOWNED) static gboolean gtk_spell_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 (either single quote, or U+2019 = 8217. */ if (!gtk_text_iter_forward_word_end (i)) return FALSE; if (gtk_text_iter_get_char (i) != '\'' && gtk_text_iter_get_char (i) != 8217) return TRUE; iter = *i; if (gtk_text_iter_forward_char (&iter) && g_unichar_isalpha (gtk_text_iter_get_char (&iter))) return (gtk_text_iter_forward_word_end (i)); return TRUE; } static gboolean gtk_spell_text_iter_backward_word_start (GtkTextIter *i) { GtkTextIter iter; if (!gtk_text_iter_backward_word_start (i)) return FALSE; iter = *i; if (g_unichar_isalpha (gtk_text_iter_get_char (&iter)) && gtk_text_iter_backward_char (&iter) && (gtk_text_iter_get_char (&iter) == '\'' || gtk_text_iter_get_char (&iter) == 8217)) return (gtk_text_iter_backward_word_start (i)); return TRUE; } #define gtk_text_iter_backward_word_start gtk_spell_text_iter_backward_word_start #define gtk_text_iter_forward_word_end gtk_spell_text_iter_forward_word_end static void check_word (GtkSpellChecker *spell, GtkTextIter *start, GtkTextIter *end) { char *text; text = gtk_text_buffer_get_text (spell->priv->buffer, start, end, FALSE); if (debug) g_print ("checking: %s\n", text); if (g_unichar_isdigit (*text) == FALSE && /* don't check numbers */ enchant_dict_check (spell->priv->speller, text, strlen (text)) != 0) gtk_text_buffer_apply_tag (spell->priv->buffer, spell->priv->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 (GtkSpellChecker *spell, GtkTextIter start, GtkTextIter end, gboolean force_all) { g_return_if_fail (spell->priv->speller != NULL); /* for check_word */ /* 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 (spell->priv->buffer, &cursor, gtk_text_buffer_get_insert (spell->priv->buffer)); precursor = cursor; gtk_text_iter_backward_char (&precursor); highlight = gtk_text_iter_has_tag (&cursor, spell->priv->tag_highlight) || gtk_text_iter_has_tag (&precursor, spell->priv->tag_highlight); gtk_text_buffer_remove_tag (spell->priv->buffer, spell->priv->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); /* make sure we've actually advanced * (we don't advance in some corner cases, such as after punctuation) */ if (gtk_text_iter_equal (&wstart, &wend)) break; 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->priv->deferred_check = TRUE; } else { check_word (spell, &wstart, &wend); spell->priv->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 (GtkSpellChecker *spell, gboolean force_all) { GtkTextIter start, end; gtk_text_buffer_get_iter_at_mark (spell->priv->buffer, &start, spell->priv->mark_insert_start); gtk_text_buffer_get_iter_at_mark (spell->priv->buffer, &end, spell->priv->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, GtkSpellChecker *spell) { g_return_if_fail (buffer == spell->priv->buffer); gtk_text_buffer_move_mark (buffer, spell->priv->mark_insert_start, iter); } static void insert_text_after (GtkTextBuffer *buffer, GtkTextIter *iter, gchar *text, gint len, GtkSpellChecker *spell) { g_return_if_fail (buffer == spell->priv->buffer); 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->priv->mark_insert_start); check_range (spell, start, *iter, FALSE); gtk_text_buffer_move_mark (buffer, spell->priv->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, GtkSpellChecker *spell) { g_return_if_fail (buffer == spell->priv->buffer); if (debug) g_print ("delete\n"); check_range (spell, *start, *end, FALSE); } static void mark_set (GtkTextBuffer *buffer, GtkTextIter *iter, GtkTextMark *mark, GtkSpellChecker *spell) { g_return_if_fail (buffer == spell->priv->buffer); /* if the cursor has moved and there is a deferred check so handle it now */ if ((mark == gtk_text_buffer_get_insert (buffer)) && spell->priv->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 add_to_dictionary (GtkWidget *menuitem, GtkSpellChecker *spell) { char *word; GtkTextIter start, end; get_word_extents_from_mark (spell->priv->buffer, &start, &end, spell->priv->mark_click); word = gtk_text_buffer_get_text (spell->priv->buffer, &start, &end, FALSE); enchant_dict_add (spell->priv->speller, word, strlen (word)); gtk_spell_checker_recheck_all (spell); g_free (word); } static void ignore_all (GtkWidget *menuitem, GtkSpellChecker *spell) { char *word; GtkTextIter start, end; get_word_extents_from_mark (spell->priv->buffer, &start, &end, spell->priv->mark_click); word = gtk_text_buffer_get_text (spell->priv->buffer, &start, &end, FALSE); enchant_dict_add_to_session (spell->priv->speller, word, strlen (word)); gtk_spell_checker_recheck_all (spell); g_free (word); } static void replace_word (GtkWidget *menuitem, GtkSpellChecker *spell) { char *oldword; const char *newword; GtkTextIter start, end; get_word_extents_from_mark (spell->priv->buffer, &start, &end, spell->priv->mark_click); oldword = gtk_text_buffer_get_text (spell->priv->buffer, &start, &end, FALSE); newword = gtk_label_get_text (GTK_LABEL (gtk_bin_get_child (GTK_BIN (menuitem)))); 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->priv->buffer); gtk_text_buffer_delete (spell->priv->buffer, &start, &end); gtk_text_buffer_insert (spell->priv->buffer, &start, newword, -1); gtk_text_buffer_end_user_action (spell->priv->buffer); enchant_dict_store_replacement (spell->priv->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 (GtkSpellChecker *spell, const char *word, GtkWidget *topmenu) { g_return_if_fail (spell->priv->speller != NULL); GtkWidget *menu; GtkWidget *mi; char **suggestions; size_t n_suggs, i; char *label; menu = topmenu; gint menu_position = 0; suggestions = enchant_dict_suggest (spell->priv->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), _("(no suggestions)")); 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 (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->priv->speller, suggestions); /* + Add to Dictionary */ label = g_strdup_printf (_("Add \"%s\" to Dictionary"), word); #if GTK_CHECK_VERSION(3,9,0) mi = gtk_menu_item_new_with_label (label); #else mi = gtk_image_menu_item_new_with_label (label); gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (mi), gtk_image_new_from_stock (GTK_STOCK_ADD, GTK_ICON_SIZE_MENU)); #endif g_free (label); g_signal_connect (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 */ #if GTK_CHECK_VERSION(3,9,0) mi = gtk_menu_item_new_with_label (_("Ignore All")); #else 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)); #endif g_signal_connect (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 (GtkSpellChecker *spell, const char *word) { GtkWidget *topmenu; topmenu = gtk_menu_new (); add_suggestion_menus (spell, word, topmenu); return topmenu; } static void language_change_callback (GtkCheckMenuItem *mi, GtkSpellChecker* spell) { if (gtk_check_menu_item_get_active (mi)) { GError* error = NULL; gchar *name; g_object_get (G_OBJECT (mi), "name", &name, NULL); gtk_spell_checker_set_language (spell, name, &error); g_signal_emit (spell, signals[LANGUAGE_CHANGED], 0, spell->priv->lang); 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 (GtkSpellChecker *spell) { GtkWidget *active_item = NULL, *menu = gtk_menu_new (); GList *langs; GtkWidget *mi; 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; #ifdef HAVE_ISO_CODES if (spell->priv->decode_codes == TRUE) { const gchar *lang_name = "\0"; const gchar *country_name = "\0"; gchar *label; codetable_lookup (lang_tag, &lang_name, &country_name); if (strlen (country_name) != 0) label = g_strdup_printf ("%s (%s)", lang_name, country_name); else label = g_strdup_printf ("%s", lang_name); mi = gtk_radio_menu_item_new_with_label (menu_group, label); g_free (label); } else #endif 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 (spell->priv->lang && strcmp (spell->priv->lang, lang_tag) == 0) active_item = mi; 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); else { /* For the situation where no language is active (i.e. * spell->priv->lang == NULL), create a "None" item which is active. */ mi = gtk_radio_menu_item_new_with_label (menu_group, _("None")); gtk_menu_shell_append (GTK_MENU_SHELL (menu), mi); gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (mi), TRUE); gtk_widget_show (mi); } /* Connect signals to menu items after determining which one is active, * since otherwise the signal is potentially already fired once (since the * first item added to the group is active by default. */ for (; menu_group; menu_group = menu_group->next) { mi = menu_group->data; if (!gtk_check_menu_item_get_active (GTK_CHECK_MENU_ITEM (mi))) g_signal_connect (mi, "activate", G_CALLBACK (language_change_callback), spell); } g_list_free (languages_cb_struct.langs); return menu; } static void populate_popup (GtkTextView *textview, GtkMenu *menu, GtkSpellChecker *spell) { g_return_if_fail (spell->priv->view == textview); 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->priv->buffer, &start, &end, spell->priv->mark_click); /* if our highlight algorithm ever messes up, * this isn't correct, either. */ if (!gtk_text_iter_has_tag (&start, spell->priv->tag_highlight)) return; /* word wasn't misspelled. */ /* then, on top of it, the suggestions */ word = gtk_text_buffer_get_text (spell->priv->buffer, &start, &end, FALSE); add_suggestion_menus (spell, 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, GtkSpellChecker *spell) { g_return_val_if_fail (spell->priv->view == view, FALSE); g_return_val_if_fail (spell->priv->buffer == gtk_text_view_get_buffer (view), FALSE); if (event->button == 3) { gint x, y; GtkTextIter iter; /* handle deferred check if it exists */ if (spell->priv->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 (spell->priv->buffer, spell->priv->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 +F10 by default). In this case we want to set * spell->priv->mark_click to the cursor position. */ static gboolean popup_menu_event (GtkTextView *view, GtkSpellChecker *spell) { g_return_val_if_fail (spell->priv->view == view, FALSE); GtkTextIter iter; gtk_text_buffer_get_iter_at_mark (spell->priv->buffer, &iter, gtk_text_buffer_get_insert (spell->priv->buffer)); gtk_text_buffer_move_mark (spell->priv->buffer, spell->priv->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) { GtkSpellChecker *spell = user_data; g_free (spell->priv->lang); spell->priv->lang = g_strdup (lang_tag); } static gboolean set_language_internal (GtkSpellChecker *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, GTK_SPELL_ERROR, GTK_SPELL_ERROR_BACKEND, _("enchant error for language: %s"), lang); return FALSE; } if (spell->priv->speller) enchant_broker_free_dict (broker, spell->priv->speller); spell->priv->speller = dict; enchant_dict_describe (dict, set_lang_from_dict, spell); return TRUE; } /* changes the buffer * a NULL buffer is acceptable and will only release the current one */ static void set_buffer (GtkSpellChecker *spell, GtkTextBuffer *buffer) { GtkTextIter start, end; if (spell->priv->buffer) { g_signal_handlers_disconnect_matched (spell->priv->buffer, G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, spell); gtk_text_buffer_get_bounds (spell->priv->buffer, &start, &end); gtk_text_buffer_remove_tag (spell->priv->buffer, spell->priv->tag_highlight, &start, &end); spell->priv->tag_highlight = NULL; gtk_text_buffer_delete_mark (spell->priv->buffer, spell->priv->mark_insert_start); spell->priv->mark_insert_start = NULL; gtk_text_buffer_delete_mark (spell->priv->buffer, spell->priv->mark_insert_end); spell->priv->mark_insert_end = NULL; gtk_text_buffer_delete_mark (spell->priv->buffer, spell->priv->mark_click); spell->priv->mark_click = NULL; g_object_unref (spell->priv->buffer); } spell->priv->buffer = buffer; if (spell->priv->buffer) { g_object_ref (spell->priv->buffer); g_signal_connect (spell->priv->buffer, "insert-text", G_CALLBACK (insert_text_before), spell); g_signal_connect_after (spell->priv->buffer, "insert-text", G_CALLBACK (insert_text_after), spell); g_signal_connect_after (spell->priv->buffer, "delete-range", G_CALLBACK (delete_range_after), spell); g_signal_connect (spell->priv->buffer, "mark-set", G_CALLBACK (mark_set), spell); GtkTextTagTable *tagtable = gtk_text_buffer_get_tag_table (spell->priv->buffer); spell->priv->tag_highlight = gtk_text_tag_table_lookup (tagtable, GTK_SPELL_MISSPELLED_TAG); if (spell->priv->tag_highlight == NULL) { spell->priv->tag_highlight = gtk_text_buffer_create_tag (spell->priv->buffer, GTK_SPELL_MISSPELLED_TAG, "underline", PANGO_UNDERLINE_ERROR, 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->priv->buffer, &start, &end); spell->priv->mark_insert_start = gtk_text_buffer_create_mark (spell->priv->buffer, "gtkspell-insert-start", &start, TRUE); spell->priv->mark_insert_end = gtk_text_buffer_create_mark (spell->priv->buffer, "gtkspell-insert-end", &start, TRUE); spell->priv->mark_click = gtk_text_buffer_create_mark (spell->priv->buffer, "gtkspell-click", &start, TRUE); spell->priv->deferred_check = FALSE; /* now check the entire text buffer. */ gtk_spell_checker_recheck_all (spell); } } static void buffer_changed (GtkTextView *view, GParamSpec *pspec, GtkSpellChecker *spell) { g_return_if_fail (spell->priv->view == view); GtkTextBuffer *buf = gtk_text_view_get_buffer (view); if (!buf) gtk_spell_checker_detach (spell); else set_buffer (spell, buf); } static void gtk_spell_checker_set_property (GObject *object, guint propid, const GValue *value, GParamSpec *pspec) { GtkSpellChecker *spell = GTK_SPELL_CHECKER (object); switch (propid) { case PROP_DECODE_LANGUAGE_CODES: spell->priv->decode_codes = g_value_get_boolean (value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, propid, pspec); break; } } static void gtk_spell_checker_get_property (GObject *object, guint propid, GValue *value, GParamSpec *pspec) { GtkSpellChecker *spell = GTK_SPELL_CHECKER (object); switch (propid) { case PROP_DECODE_LANGUAGE_CODES: g_value_set_boolean (value, spell->priv->decode_codes); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, propid, pspec); break; } } static void gtk_spell_checker_class_init (GtkSpellCheckerClass *klass) { g_type_class_add_private (klass, sizeof (GtkSpellCheckerPrivate)); GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->dispose = gtk_spell_checker_dispose; object_class->finalize = gtk_spell_checker_finalize; object_class->set_property = gtk_spell_checker_set_property; object_class->get_property = gtk_spell_checker_get_property; /** * GtkSpellChecker::language-changed: * @spell: the #GtkSpellChecker object which received the signal. * @lang: the new language which was selected. * * The ::language-changed signal is emitted when the user selects * a new spelling language from the context menu. * */ signals[LANGUAGE_CHANGED] = g_signal_new ("language-changed", G_OBJECT_CLASS_TYPE (object_class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (GtkSpellCheckerClass, language_changed), NULL, NULL, g_cclosure_marshal_VOID__STRING, G_TYPE_NONE, 1, G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE); /** * GtkSpellChecker::decode-language-codes: * * Whether to show decoded language codes in the context menu * (requires the iso-codes package). */ g_object_class_install_property (object_class, PROP_DECODE_LANGUAGE_CODES, g_param_spec_boolean ("decode-language-codes", "Decode language codes", "Whether to show decoded language codes in the "\ "context menu (requires the iso-codes package).", FALSE, G_PARAM_READWRITE)); } static void gtk_spell_checker_init (GtkSpellChecker *self) { self->priv = GTK_SPELL_CHECKER_GET_PRIVATE (self); self->priv->view = NULL; self->priv->buffer = NULL; self->priv->tag_highlight = NULL; self->priv->mark_insert_start = NULL; self->priv->mark_insert_end = NULL; self->priv->mark_click = NULL; self->priv->deferred_check = FALSE; self->priv->speller = NULL; self->priv->lang = NULL; #ifdef ENABLE_NLS bindtextdomain (PACKAGE_NAME, PACKAGE_LOCALE_DIR); bind_textdomain_codeset (PACKAGE_NAME, "UTF-8"); #endif if (!broker) { broker = enchant_broker_init (); broker_ref_cnt = 0; } broker_ref_cnt++; #ifdef HAVE_ISO_CODES if (codetable_ref_cnt == 0) codetable_init (); codetable_ref_cnt++; #endif set_language_internal (self, NULL, NULL); } static void gtk_spell_checker_dispose (GObject *object) { GtkSpellChecker *spell = GTK_SPELL_CHECKER (object); gtk_spell_checker_detach (spell); G_INITIALLY_UNOWNED_CLASS (gtk_spell_checker_parent_class)->dispose (object); } static void gtk_spell_checker_finalize (GObject *object) { GtkSpellChecker *spell = GTK_SPELL_CHECKER (object); if (broker) { if (spell->priv->speller) enchant_broker_free_dict (broker, spell->priv->speller); broker_ref_cnt--; if (broker_ref_cnt == 0) { enchant_broker_free (broker); broker = NULL; } #ifdef HAVE_ISO_CODES codetable_ref_cnt--; if (codetable_ref_cnt == 0) codetable_free (); #endif } g_free (spell->priv->lang); G_INITIALLY_UNOWNED_CLASS (gtk_spell_checker_parent_class)->finalize (object); } /** * gtk_spell_checker_new: * * Create a new #GtkSpellChecker object. * * Returns: a new #GtkSpellChecker object. */ GtkSpellChecker* gtk_spell_checker_new (void) { return g_object_new (GTK_SPELL_TYPE_CHECKER, NULL); } /** * gtk_spell_checker_attach: * @spell: A #GtkSpellChecker. * @view: The #GtkTextView to attach to. * * Attach #GtkSpellChecker object to @view. * * Note: Please read the tutorial section of the documentation to make sure * you don't leak references! * * Returns: TRUE on success, FALSE on failure. */ gboolean gtk_spell_checker_attach (GtkSpellChecker *spell, GtkTextView *view) { g_return_val_if_fail (GTK_SPELL_IS_CHECKER (spell), FALSE); g_return_val_if_fail (GTK_IS_TEXT_VIEW (view), FALSE); g_return_val_if_fail (gtk_text_view_get_buffer (view), FALSE); g_return_val_if_fail (spell->priv->view == NULL, FALSE); /* ensure no existing instance is attached */ GtkSpellChecker *attached; attached = g_object_get_data (G_OBJECT (view), GTK_SPELL_OBJECT_KEY); g_return_val_if_fail (attached == NULL, FALSE); /* attach to the widget */ spell->priv->view = view; g_object_ref (view); g_object_ref_sink (spell); g_object_set_data (G_OBJECT (view), GTK_SPELL_OBJECT_KEY, spell); g_signal_connect_swapped (view, "destroy", G_CALLBACK (gtk_spell_checker_detach), spell); g_signal_connect (view, "button-press-event", G_CALLBACK (button_press_event), spell); g_signal_connect (view, "populate-popup", G_CALLBACK (populate_popup), spell); g_signal_connect (view, "popup-menu", G_CALLBACK (popup_menu_event), spell); g_signal_connect (view, "notify::buffer", G_CALLBACK (buffer_changed), spell); set_buffer (spell, gtk_text_view_get_buffer (view)); return TRUE; } /** * gtk_spell_checker_detach: * @spell: A #GtkSpellChecker. * * Detaches this #GtkSpellChecker from its #GtkTextView. Use * gtk_spell_checker_get_from_text_view () to retrieve a #GtkSpellChecker from * a #GtkTextView. If the #GtkSpellChecker is not attached to any #GtkTextView, * the function silently exits. * * Note: if the #GtkSpellChecker is owned by the #GtkTextView, you must * take a reference to it to prevent it from being automatically destroyed. * Please read the tutorial section of the documentation! */ void gtk_spell_checker_detach (GtkSpellChecker *spell) { g_return_if_fail (GTK_SPELL_IS_CHECKER (spell)); if (spell->priv->view == NULL) return; g_signal_handlers_disconnect_matched (spell->priv->view, G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, spell); g_object_set_data (G_OBJECT (spell->priv->view), GTK_SPELL_OBJECT_KEY, NULL); g_object_unref (spell->priv->view); spell->priv->view = NULL; set_buffer (spell, NULL); spell->priv->deferred_check = FALSE; g_object_unref (spell); } /** * gtk_spell_checker_get_from_text_view: * @view: A #GtkTextView. * * Retrieves the #GtkSpellChecker object attached to a text view. * * Returns: (transfer none): the #GtkSpellChecker object, or %NULL if there is no #GtkSpellChecker * attached to @view. */ GtkSpellChecker* gtk_spell_checker_get_from_text_view (GtkTextView *view) { g_return_val_if_fail (GTK_IS_TEXT_VIEW (view), NULL); return g_object_get_data (G_OBJECT (view), GTK_SPELL_OBJECT_KEY); } /** * gtk_spell_checker_get_language: * @spell: a #GtkSpellChecker * * Fetches the current language. * * Returns: the current language. This string is * owned by the spell object and must not be modified or freed. **/ const gchar* gtk_spell_checker_get_language (GtkSpellChecker *spell) { g_return_val_if_fail (GTK_SPELL_IS_CHECKER (spell), NULL); return spell->priv->lang; } /** * gtk_spell_checker_get_language_list: * * Requests the list of available languages from the enchant broker. * * Returns: (transfer full) (element-type utf8): a #GList of the available languages. * Use g_list_free_full with g_free to free the list after use. * * Since: 3.0.3 */ GList* gtk_spell_checker_get_language_list (void) { struct _languages_cb_struct languages_cb_struct; if (!broker) { broker = enchant_broker_init(); broker_ref_cnt = 0; } languages_cb_struct.langs = NULL; enchant_broker_list_dicts(broker, dict_describe_cb, &languages_cb_struct); if (broker_ref_cnt == 0) { enchant_broker_free (broker); broker = NULL; } return languages_cb_struct.langs; } /** * gtk_spell_checker_decode_language_code: * @lang: The language locale specifier (i.e. "en_US"). * * Translates the language code to a human readable format * (i.e. "en_US" -> "English (United States)"). * Note: If the iso-codes package is not available, the unchanged code is * returned. * * Returns: (transfer full): The translated language specifier. Use g_free to * free the returned string after use. * * Since: 3.0.3 */ gchar* gtk_spell_checker_decode_language_code (const gchar *lang) { gchar* result; #ifdef HAVE_ISO_CODES const gchar *lang_name = "\0"; const gchar *country_name = "\0"; if (codetable_ref_cnt == 0) codetable_init (); codetable_lookup (lang, &lang_name, &country_name); if (strlen (country_name) != 0) result = g_strdup_printf ("%s (%s)", lang_name, country_name); else result = g_strdup_printf ("%s", lang_name); if (codetable_ref_cnt == 0) codetable_free (); #else result = g_strdup (lang); #endif return result; } /** * gtk_spell_checker_set_language: * @spell: The #GtkSpellChecker object. * @lang: (allow-none): The language to use, as a locale specifier (i.e. "en_US"). * If #NULL, attempt to use the default system locale (LANG). * @error: (out) (allow-none): 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 gtk_spell_checker_set_language (GtkSpellChecker *spell, const gchar *lang, GError **error) { g_return_val_if_fail (GTK_SPELL_IS_CHECKER (spell), FALSE); if (error) g_return_val_if_fail (*error == NULL, FALSE); gboolean ret = set_language_internal (spell, lang, error); if (ret) gtk_spell_checker_recheck_all (spell); return ret; } /** * gtk_spell_checker_check_word: * @spell: The #GtkSpellChecker object. * @word: The word to check. * * Check the specified word. * * Returns: TRUE if the word is correctly spelled, FALSE otherwise. * * Since: 3.0.8 */ gboolean gtk_spell_checker_check_word (GtkSpellChecker *spell, const gchar *word) { if (g_unichar_isdigit (*word) == TRUE || /* don't check numbers */ enchant_dict_check (spell->priv->speller, word, strlen (word)) == 0) return TRUE; return FALSE; } /** * gtk_spell_checker_recheck_all: * @spell: The #GtkSpellChecker object. * * Recheck the spelling in the entire buffer. */ void gtk_spell_checker_recheck_all (GtkSpellChecker *spell) { g_return_if_fail (GTK_SPELL_IS_CHECKER (spell)); GtkTextIter start, end; if (spell->priv->buffer) { gtk_text_buffer_get_bounds (spell->priv->buffer, &start, &end); check_range (spell, start, end, TRUE); } } /** * gtk_spell_checker_add_to_dictionary: * @spell: The #GtkSpellChecker object. * @word: The word to add to the user dictionary. * * Add the specified word to the user dictionary. * * Since: 3.0.9 */ void gtk_spell_checker_add_to_dictionary (GtkSpellChecker *spell, const gchar *word) { enchant_dict_add (spell->priv->speller, word, strlen (word)); gtk_spell_checker_recheck_all (spell); } /** * gtk_spell_checker_ignore_word: * @spell: The #GtkSpellChecker object. * @word: The word to add to the user ignore list. * * Add the specified word to the user ignore list. * * Since: 3.0.9 */ void gtk_spell_checker_ignore_word (GtkSpellChecker *spell, const gchar *word) { enchant_dict_add_to_session (spell->priv->speller, word, strlen (word)); gtk_spell_checker_recheck_all (spell); } /** * gtk_spell_checker_get_suggestions: * @spell: A #GtkSpellChecker. * @word: The word for which to fetch suggestions * * Retreives a list of spelling suggestions for the specified word. * * Returns: (transfer full) (element-type utf8): the list of spelling * suggestions for the specified word, or NULL if there are no suggestions. * * Since: 3.0.8 */ GList* gtk_spell_checker_get_suggestions (GtkSpellChecker *spell, const gchar* word) { char **suggestions; size_t n_suggs, i; GList* result = NULL; suggestions = enchant_dict_suggest (spell->priv->speller, word, strlen (word), &n_suggs); for (i = 0; i < n_suggs; ++i) { result = g_list_append (result, g_strdup (suggestions[i])); } return result; } /** * gtk_spell_checker_get_suggestions_menu: * @spell: A #GtkSpellChecker. * @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: (transfer full): the #GtkMenu widget, or %NULL if there is no need for a menu */ GtkWidget* gtk_spell_checker_get_suggestions_menu (GtkSpellChecker *spell, GtkTextIter *iter) { g_return_val_if_fail (GTK_SPELL_IS_CHECKER (spell), NULL); g_return_val_if_fail (iter != NULL, NULL); GtkWidget *submenu = NULL; GtkTextIter start, end; start = *iter; /* use the same lazy test, with same risk, as does the default menu arrangement */ if (gtk_text_iter_has_tag (&start, spell->priv->tag_highlight)) { /* word was mis-spelt */ gchar *badword; /* in case a fix is requested, move the attention-point */ gtk_text_buffer_move_mark (spell->priv->buffer, spell->priv->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->priv->buffer, &start, &end, FALSE); submenu = build_suggestion_menu (spell, badword); gtk_widget_show (submenu); g_free (badword); } return submenu; } GQuark gtk_spell_error_quark (void) { static GQuark q = 0; if (q == 0) q = g_quark_from_static_string ("gtkspell-error-quark"); return q; } GType gtk_spell_error_get_type (void) { static GType etype = 0; if (G_UNLIKELY(etype == 0)) { static const GEnumValue values[] = { { GTK_SPELL_ERROR_BACKEND, "GTK_SPELL_ERROR_BACKEND", "backend" }, { 0, NULL, NULL } }; etype = g_enum_register_static (g_intern_static_string ("GtkSpellError"), values); } return etype; }