/*
* 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);
}