diff options
Diffstat (limited to 'e-util/e-spell-checker.c')
-rw-r--r-- | e-util/e-spell-checker.c | 783 |
1 files changed, 783 insertions, 0 deletions
diff --git a/e-util/e-spell-checker.c b/e-util/e-spell-checker.c new file mode 100644 index 0000000000..c781672103 --- /dev/null +++ b/e-util/e-spell-checker.c @@ -0,0 +1,783 @@ +/* + * e-spell-checker.c + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser 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. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include "e-spell-checker.h" +#include "e-spell-dictionary.h" + +#include <libebackend/libebackend.h> +#include <webkit/webkitspellchecker.h> +#include <pango/pango.h> +#include <gtk/gtk.h> +#include <string.h> + +#define E_SPELL_CHECKER_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_SPELL_CHECKER, ESpellCheckerPrivate)) + +#define MAX_SUGGESTIONS 10 + +struct _ESpellCheckerPrivate { + EnchantBroker *broker; + GHashTable *active_dictionaries; + GHashTable *dictionaries_cache; + gboolean dictionaries_loaded; + + /* We retain ownership of the EnchantDict's since they + * have to be freed through enchant_broker_free_dict() + * and we also own the EnchantBroker. */ + GHashTable *enchant_dicts; +}; + +enum { + PROP_0, + PROP_ACTIVE_LANGUAGES +}; + +/* Forward Declarations */ +static void e_spell_checker_init_webkit_checker + (WebKitSpellCheckerInterface *interface); + +G_DEFINE_TYPE_EXTENDED ( + ESpellChecker, + e_spell_checker, + G_TYPE_OBJECT, + 0, + G_IMPLEMENT_INTERFACE ( + E_TYPE_EXTENSIBLE, NULL) + G_IMPLEMENT_INTERFACE ( + WEBKIT_TYPE_SPELL_CHECKER, + e_spell_checker_init_webkit_checker)) + +/** + * ESpellChecker: + * + * #ESpellChecker represents a spellchecker in Evolution. It can be used as a + * provider for dictionaries. It also implements #WebKitSpellCheckerInterface, + * so it can be set as a default spell-checker to WebKit editors + */ + +static gboolean +spell_checker_enchant_dicts_foreach_cb (gpointer key, + gpointer value, + gpointer user_data) +{ + EnchantDict *enchant_dict = value; + EnchantBroker *enchant_broker = user_data; + + enchant_broker_free_dict (enchant_broker, enchant_dict); + + return TRUE; +} + +static void +wksc_check_spelling (WebKitSpellChecker *webkit_checker, + const gchar *word, + gint *misspelling_location, + gint *misspelling_length) +{ + ESpellChecker *checker = E_SPELL_CHECKER (webkit_checker); + GHashTable *active_dictionaries; + PangoLanguage *language; + PangoLogAttr *attrs; + gint length, ii; + + active_dictionaries = checker->priv->active_dictionaries; + if (g_hash_table_size (active_dictionaries) == 0) + return; + + length = g_utf8_strlen (word, -1); + + language = pango_language_get_default (); + attrs = g_new (PangoLogAttr, length + 1); + + pango_get_log_attrs (word, -1, -1, language, attrs, length + 1); + + for (ii = 0; ii < length + 1; ii++) { + /* We go through each character until we find an is_word_start, + * then we get into an inner loop to find the is_word_end + * corresponding */ + if (attrs[ii].is_word_start) { + gboolean word_recognized; + gint start = ii; + gint end = ii; + gint word_length; + gchar *cstart; + gint bytes; + gchar *new_word; + + while (attrs[end].is_word_end < 1) + end++; + + word_length = end - start; + /* Set the iterator to be at the current word + * end, so we don't check characters twice. */ + ii = end; + + cstart = g_utf8_offset_to_pointer (word, start); + bytes = g_utf8_offset_to_pointer (word, end) - cstart; + new_word = g_new0 (gchar, bytes + 1); + + g_utf8_strncpy (new_word, cstart, word_length); + + word_recognized = e_spell_checker_check_word ( + checker, new_word, strlen (new_word)); + + if (word_recognized) { + if (misspelling_location != NULL) + *misspelling_location = -1; + if (misspelling_length != NULL) + *misspelling_length = 0; + } else { + if (misspelling_location != NULL) + *misspelling_location = start; + if (misspelling_length != NULL) + *misspelling_length = word_length; + } + + g_free (new_word); + } + } + + g_free (attrs); +} + +static gchar ** +wksc_get_guesses (WebKitSpellChecker *webkit_checker, + const gchar *word, + const gchar *context) +{ + ESpellChecker *checker = E_SPELL_CHECKER (webkit_checker); + GHashTable *active_dictionaries; + GList *list, *link; + gchar ** guesses; + gint ii = 0; + + guesses = g_new0 (gchar *, MAX_SUGGESTIONS + 1); + + active_dictionaries = checker->priv->active_dictionaries; + list = g_hash_table_get_keys (active_dictionaries); + + for (link = list; link != NULL; link = g_list_next (link)) { + ESpellDictionary *dictionary; + GList *suggestions; + + dictionary = E_SPELL_DICTIONARY (link->data); + suggestions = e_spell_dictionary_get_suggestions ( + dictionary, word, -1); + + while (suggestions != NULL && ii < MAX_SUGGESTIONS) { + guesses[ii++] = suggestions->data; + suggestions->data = NULL; + + suggestions = g_list_delete_link ( + suggestions, suggestions); + } + + g_list_free_full (suggestions, (GDestroyNotify) g_free); + + if (ii >= MAX_SUGGESTIONS) + break; + } + + g_list_free (list); + + return guesses; +} + +static gchar * +wksc_get_autocorrect_suggestions (WebKitSpellChecker *webkit_checker, + const gchar *word) +{ + /* Not supported/needed */ + return NULL; +} + +static void +spell_checker_learn_word (WebKitSpellChecker *webkit_checker, + const gchar *word) +{ + /* Carefully, this will add the word to all active dictionaries! */ + + ESpellChecker *checker; + GHashTable *active_dictionaries; + GList *list, *link; + + checker = E_SPELL_CHECKER (webkit_checker); + active_dictionaries = checker->priv->active_dictionaries; + list = g_hash_table_get_keys (active_dictionaries); + + for (link = list; link != NULL; link = g_list_next (link)) { + ESpellDictionary *dictionary; + + dictionary = E_SPELL_DICTIONARY (link->data); + e_spell_dictionary_learn_word (dictionary, word, -1); + } + + g_list_free (list); +} + +static void +spell_checker_ignore_word (WebKitSpellChecker *webkit_checker, + const gchar *word) +{ + /* Carefully, this will add the word to all active dictionaries */ + + ESpellChecker *checker; + GHashTable *active_dictionaries; + GList *list, *link; + + checker = E_SPELL_CHECKER (webkit_checker); + active_dictionaries = checker->priv->active_dictionaries; + list = g_hash_table_get_keys (active_dictionaries); + + for (link = list; link != NULL; link = g_list_next (link)) { + ESpellDictionary *dictionary; + + dictionary = E_SPELL_DICTIONARY (link->data); + e_spell_dictionary_ignore_word (dictionary, word, -1); + } + + g_list_free (list); +} + +static void +wksc_update_languages (WebKitSpellChecker *webkit_checker, + const gchar *languages) +{ + ESpellChecker *checker; + GHashTable *active_dictionaries; + GQueue queue = G_QUEUE_INIT; + + checker = E_SPELL_CHECKER (webkit_checker); + active_dictionaries = checker->priv->active_dictionaries; + + if (languages != NULL) { + gchar **langs; + gint ii; + + langs = g_strsplit (languages, ",", -1); + for (ii = 0; langs[ii] != NULL; ii++) { + ESpellDictionary *dictionary; + + dictionary = e_spell_checker_ref_dictionary ( + checker, langs[ii]); + if (dictionary != NULL) + g_queue_push_tail (&queue, dictionary); + } + g_strfreev (langs); + } else { + ESpellDictionary *dictionary; + PangoLanguage *pango_language; + const gchar *language; + + pango_language = gtk_get_default_language (); + language = pango_language_to_string (pango_language); + dictionary = e_spell_checker_ref_dictionary (checker, language); + + if (dictionary == NULL) { + GList *list; + + list = e_spell_checker_list_available_dicts (checker); + if (list != NULL) { + dictionary = g_object_ref (list->data); + g_list_free (list); + } + } + + if (dictionary != NULL) + g_queue_push_tail (&queue, dictionary); + } + + g_hash_table_remove_all (active_dictionaries); + + while (!g_queue_is_empty (&queue)) { + ESpellDictionary *dictionary; + + dictionary = g_queue_pop_head (&queue); + g_hash_table_add (active_dictionaries, dictionary); + } + + g_object_notify (G_OBJECT (checker), "active-languages"); +} + +static void +spell_checker_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_ACTIVE_LANGUAGES: + g_value_take_boxed ( + value, + e_spell_checker_list_active_languages ( + E_SPELL_CHECKER (object), NULL)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +spell_checker_dispose (GObject *object) +{ + ESpellCheckerPrivate *priv; + + priv = E_SPELL_CHECKER_GET_PRIVATE (object); + + g_hash_table_remove_all (priv->active_dictionaries); + g_hash_table_remove_all (priv->dictionaries_cache); + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (e_spell_checker_parent_class)->dispose (object); +} + +static void +spell_checker_finalize (GObject *object) +{ + ESpellCheckerPrivate *priv; + + priv = E_SPELL_CHECKER_GET_PRIVATE (object); + + /* Freeing EnchantDicts requires help from EnchantBroker. */ + g_hash_table_foreach_remove ( + priv->enchant_dicts, + spell_checker_enchant_dicts_foreach_cb, + priv->broker); + g_hash_table_destroy (priv->enchant_dicts); + + enchant_broker_free (priv->broker); + + g_hash_table_destroy (priv->active_dictionaries); + g_hash_table_destroy (priv->dictionaries_cache); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (e_spell_checker_parent_class)->finalize (object); +} + +static void +spell_checker_constructed (GObject *object) +{ + /* Chain up to parent's constructed() method. */ + G_OBJECT_CLASS (e_spell_checker_parent_class)->constructed (object); + + e_extensible_load_extensions (E_EXTENSIBLE (object)); +} + +static void +e_spell_checker_class_init (ESpellCheckerClass *class) +{ + GObjectClass *object_class; + + g_type_class_add_private (class, sizeof (ESpellCheckerPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->get_property = spell_checker_get_property; + object_class->dispose = spell_checker_dispose; + object_class->finalize = spell_checker_finalize; + object_class->constructed = spell_checker_constructed; + + g_object_class_install_property ( + object_class, + PROP_ACTIVE_LANGUAGES, + g_param_spec_boxed ( + "active-languages", + "Active Languages", + "Active spell check language codes", + G_TYPE_STRV, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS)); +} + +static void +e_spell_checker_init_webkit_checker (WebKitSpellCheckerInterface *interface) +{ + interface->check_spelling_of_string = wksc_check_spelling; + interface->get_autocorrect_suggestions_for_misspelled_word = + wksc_get_autocorrect_suggestions; + interface->get_guesses_for_word = wksc_get_guesses; + interface->ignore_word = spell_checker_ignore_word; + interface->learn_word = spell_checker_learn_word; + interface->update_spell_checking_languages = wksc_update_languages; +} + +static void +e_spell_checker_init (ESpellChecker *checker) +{ + GHashTable *active_dictionaries; + GHashTable *dictionaries_cache; + GHashTable *enchant_dicts; + + active_dictionaries = g_hash_table_new_full ( + (GHashFunc) e_spell_dictionary_hash, + (GEqualFunc) e_spell_dictionary_equal, + (GDestroyNotify) g_object_unref, + (GDestroyNotify) NULL); + + dictionaries_cache = g_hash_table_new_full ( + (GHashFunc) g_str_hash, + (GEqualFunc) g_str_equal, + (GDestroyNotify) NULL, + (GDestroyNotify) g_object_unref); + + enchant_dicts = g_hash_table_new_full ( + (GHashFunc) g_str_hash, + (GEqualFunc) g_str_equal, + (GDestroyNotify) g_free, + (GDestroyNotify) NULL); + + checker->priv = E_SPELL_CHECKER_GET_PRIVATE (checker); + + checker->priv->broker = enchant_broker_init (); + checker->priv->active_dictionaries = active_dictionaries; + checker->priv->dictionaries_cache = dictionaries_cache; + checker->priv->enchant_dicts = enchant_dicts; +} + +/** + * e_spell_checker_new: + * + * Creates a new #ESpellChecker instance. + * + * Returns: a new #ESpellChecker + **/ +ESpellChecker * +e_spell_checker_new (void) +{ + return g_object_new (E_TYPE_SPELL_CHECKER, NULL); +} + +static void +list_enchant_dicts (const gchar * const lang_tag, + const gchar * const provider_name, + const gchar * const provider_desc, + const gchar * const provider_file, + gpointer user_data) +{ + ESpellChecker *checker = user_data; + EnchantDict *enchant_dict; + + enchant_dict = enchant_broker_request_dict ( + checker->priv->broker, lang_tag); + if (enchant_dict != NULL) { + ESpellDictionary *dictionary; + const gchar *code; + + /* Note that we retain ownership of the EnchantDict. + * Since EnchantDict is not reference counted, we're + * merely loaning the pointer to ESpellDictionary. */ + dictionary = e_spell_dictionary_new (checker, enchant_dict); + code = e_spell_dictionary_get_code (dictionary); + + g_hash_table_insert ( + checker->priv->dictionaries_cache, + (gpointer) code, dictionary); + + g_hash_table_insert ( + checker->priv->enchant_dicts, + g_strdup (code), enchant_dict); + } +} + +/** + * e_spell_checker_list_available_dicts: + * @checker: An #ESpellChecker + * + * Returns list of all dictionaries available to the actual + * spell-checking backend. + * + * Returns: new copy of #GList of #ESpellDictionary. The dictionaries are + * owned by the @checker and should not be free'd. The list should be freed + * using g_list_free() when not neede anymore. [transfer-list] + */ +GList * +e_spell_checker_list_available_dicts (ESpellChecker *checker) +{ + GList *list; + + g_return_val_if_fail (E_IS_SPELL_CHECKER (checker), NULL); + + if (!checker->priv->dictionaries_loaded) { + enchant_broker_list_dicts ( + checker->priv->broker, + list_enchant_dicts, checker); + checker->priv->dictionaries_loaded = TRUE; + } + + list = g_hash_table_get_values (checker->priv->dictionaries_cache); + + return g_list_sort (list, (GCompareFunc) e_spell_dictionary_compare); +} + +/** + * e_spell_checker_ref_dictionary: + * @checker: an #ESpellChecker + * @language_code: (allow-none): language code of a dictionary, or %NULL + * + * Tries to find an #ESpellDictionary for given @language_code. + * If @language_code is %NULL, the function will return a default + * #ESpellDictionary. + * + * Returns: an #ESpellDictionary for @language_code + */ +ESpellDictionary * +e_spell_checker_ref_dictionary (ESpellChecker *checker, + const gchar *language_code) +{ + ESpellDictionary *dictionary; + GList *list; + + g_return_val_if_fail (E_IS_SPELL_CHECKER (checker), NULL); + + /* If the cache has not yet been initialized, do so - we will need + * it anyway, Otherwise is this call very cheap */ + list = e_spell_checker_list_available_dicts (checker); + + if (language_code == NULL) { + dictionary = (list != NULL) ? list->data : NULL; + } else { + dictionary = g_hash_table_lookup ( + checker->priv->dictionaries_cache, + language_code); + } + + if (dictionary != NULL) + g_object_ref (dictionary); + + g_list_free (list); + + return dictionary; +} + +/** + * e_spell_checker_get_enchant_dict: + * @checker: an #ESpellChecker + * @language_code: language code of a dictionary, or %NULL + * + * Returns the #EnchantDict for @language_code, or %NULL if there is none. + * + * Returns: the #EnchantDict for @language_code, or %NULL + **/ +EnchantDict * +e_spell_checker_get_enchant_dict (ESpellChecker *checker, + const gchar *language_code) +{ + g_return_val_if_fail (E_IS_SPELL_CHECKER (checker), NULL); + g_return_val_if_fail (language_code != NULL, NULL); + + return g_hash_table_lookup ( + checker->priv->enchant_dicts, language_code); +} + +gboolean +e_spell_checker_get_language_active (ESpellChecker *checker, + const gchar *language_code) +{ + ESpellDictionary *dictionary; + GHashTable *active_dictionaries; + gboolean active; + + g_return_val_if_fail (E_IS_SPELL_CHECKER (checker), FALSE); + g_return_val_if_fail (language_code != NULL, FALSE); + + dictionary = e_spell_checker_ref_dictionary (checker, language_code); + g_return_val_if_fail (dictionary != NULL, FALSE); + + active_dictionaries = checker->priv->active_dictionaries; + active = g_hash_table_contains (active_dictionaries, dictionary); + + g_object_unref (dictionary); + + return active; +} + +void +e_spell_checker_set_language_active (ESpellChecker *checker, + const gchar *language_code, + gboolean active) +{ + ESpellDictionary *dictionary; + GHashTable *active_dictionaries; + gboolean is_active; + + g_return_if_fail (E_IS_SPELL_CHECKER (checker)); + g_return_if_fail (language_code != NULL); + + dictionary = e_spell_checker_ref_dictionary (checker, language_code); + g_return_if_fail (dictionary != NULL); + + active_dictionaries = checker->priv->active_dictionaries; + is_active = g_hash_table_contains (active_dictionaries, dictionary); + + if (active && !is_active) { + g_object_ref (dictionary); + g_hash_table_add (active_dictionaries, dictionary); + g_object_notify (G_OBJECT (checker), "active-languages"); + } else if (!active && is_active) { + g_hash_table_remove (active_dictionaries, dictionary); + g_object_notify (G_OBJECT (checker), "active-languages"); + } + + g_object_unref (dictionary); +} + +/** + * e_spell_checker_list_active_languages: + * @checker: an #ESpellChecker + * @n_languages: return location for the number of active languages, or %NULL + * + * Returns a %NULL-terminated array of language codes actively being used + * for spell checking. Free the returned array with g_strfreev(). + * + * Returns: a %NULL-teriminated array of language codes + **/ +gchar ** +e_spell_checker_list_active_languages (ESpellChecker *checker, + guint *n_languages) +{ + GHashTable *active_dictionaries; + GList *list, *link; + gchar **active_languages; + guint size; + gint ii = 0; + + g_return_val_if_fail (E_IS_SPELL_CHECKER (checker), NULL); + + active_dictionaries = checker->priv->active_dictionaries; + list = g_hash_table_get_keys (active_dictionaries); + size = g_hash_table_size (active_dictionaries); + + active_languages = g_new0 (gchar *, size + 1); + + list = g_list_sort (list, (GCompareFunc) e_spell_dictionary_compare); + + for (link = list; link != NULL; link = g_list_next (link)) { + ESpellDictionary *dictionary; + const gchar *language_code; + + dictionary = E_SPELL_DICTIONARY (link->data); + language_code = e_spell_dictionary_get_code (dictionary); + active_languages[ii++] = g_strdup (language_code); + } + + if (n_languages != NULL) + *n_languages = size; + + g_list_free (list); + + return active_languages; +} + +/** + * e_spell_checker_count_active_languages: + * @checker: an #ESpellChecker + * + * Returns the number of languages actively being used for spell checking. + * + * Returns: number of active spell checking languages + **/ +guint +e_spell_checker_count_active_languages (ESpellChecker *checker) +{ + g_return_val_if_fail (E_IS_SPELL_CHECKER (checker), 0); + + return g_hash_table_size (checker->priv->active_dictionaries); +} + +/** + * e_spell_checker_check_word: + * @checker: an #SpellChecker + * @word: a word to spell-check + * @length: length of @word in bytes or -1 when %NULL-terminated + * + * Calls e_spell_dictionary_check_word() on all active dictionaries in + * @checker, and returns %TRUE if @word is recognized by any of them. + * + * Returns: %TRUE if @word is recognized, %FALSE otherwise + **/ +gboolean +e_spell_checker_check_word (ESpellChecker *checker, + const gchar *word, + gsize length) +{ + GList *list, *link; + gboolean recognized = FALSE; + + g_return_val_if_fail (E_IS_SPELL_CHECKER (checker), TRUE); + g_return_val_if_fail (word != NULL && *word != '\0', TRUE); + + list = g_hash_table_get_keys (checker->priv->active_dictionaries); + + for (link = list; link != NULL; link = g_list_next (link)) { + ESpellDictionary *dictionary; + + dictionary = E_SPELL_DICTIONARY (link->data); + if (e_spell_dictionary_check_word (dictionary, word, length)) { + recognized = TRUE; + break; + } + } + + g_list_free (list); + + return recognized; +} + +/** + * e_spell_checker_ignore_word: + * @checker: an #ESpellChecker + * @word: word to ignore for the rest of session + * + * Calls e_spell_dictionary_ignore_word() on all active dictionaries in + * the @checker. + */ +void +e_spell_checker_ignore_word (ESpellChecker *checker, + const gchar *word) +{ + WebKitSpellCheckerInterface *interface; + + g_return_if_fail (E_IS_SPELL_CHECKER (checker)); + + interface = WEBKIT_SPELL_CHECKER_GET_IFACE (checker); + interface->ignore_word (WEBKIT_SPELL_CHECKER (checker), word); +} + +/** + * e_spell_checker_learn_word: + * @checker: an #ESpellChecker + * @word: word to learn + * + * Calls e_spell_dictionary_learn_word() on all active dictionaries in + * the @checker. + */ +void +e_spell_checker_learn_word (ESpellChecker *checker, + const gchar *word) +{ + WebKitSpellCheckerInterface *interface; + + g_return_if_fail (E_IS_SPELL_CHECKER (checker)); + + interface = WEBKIT_SPELL_CHECKER_GET_IFACE (checker); + interface->learn_word (WEBKIT_SPELL_CHECKER (checker), word); +} |