diff options
Diffstat (limited to 'src/fe-gtk/sexy-spell-entry.c')
-rw-r--r-- | src/fe-gtk/sexy-spell-entry.c | 1329 |
1 files changed, 1329 insertions, 0 deletions
diff --git a/src/fe-gtk/sexy-spell-entry.c b/src/fe-gtk/sexy-spell-entry.c new file mode 100644 index 00000000..d67ffe2d --- /dev/null +++ b/src/fe-gtk/sexy-spell-entry.c @@ -0,0 +1,1329 @@ +/* + * @file libsexy/sexy-icon-entry.c Entry widget + * + * @Copyright (C) 2004-2006 Christian Hammond. + * Some of this code is from gtkspell, Copyright (C) 2002 Evan Martin. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; 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 <gtk/gtk.h> +#include "sexy-spell-entry.h" +#include <string.h> +#include <glib/gi18n.h> +#include <sys/types.h> +/*#include "gtkspell-iso-codes.h" +#include "sexy-marshal.h"*/ + +/* + * Bunch of poop to make enchant into a runtime dependency rather than a + * compile-time dependency. This makes it so I don't have to hear the + * complaints from people with binary distributions who don't get spell + * checking because they didn't check their configure output. + */ +struct EnchantDict; +struct EnchantBroker; + +typedef void (*EnchantDictDescribeFn) (const char * const lang_tag, + const char * const provider_name, + const char * const provider_desc, + const char * const provider_file, + void * user_data); + +static struct EnchantBroker * (*enchant_broker_init) (void); +static void (*enchant_broker_free) (struct EnchantBroker * broker); +static void (*enchant_broker_free_dict) (struct EnchantBroker * broker, struct EnchantDict * dict); +static void (*enchant_broker_list_dicts) (struct EnchantBroker * broker, EnchantDictDescribeFn fn, void * user_data); +static struct EnchantDict * (*enchant_broker_request_dict) (struct EnchantBroker * broker, const char *const tag); + +static void (*enchant_dict_add_to_personal) (struct EnchantDict * dict, const char *const word, ssize_t len); +static void (*enchant_dict_add_to_session) (struct EnchantDict * dict, const char *const word, ssize_t len); +static int (*enchant_dict_check) (struct EnchantDict * dict, const char *const word, ssize_t len); +static void (*enchant_dict_describe) (struct EnchantDict * dict, EnchantDictDescribeFn fn, void * user_data); +static void (*enchant_dict_free_suggestions) (struct EnchantDict * dict, char **suggestions); +static void (*enchant_dict_store_replacement) (struct EnchantDict * dict, const char *const mis, ssize_t mis_len, const char *const cor, ssize_t cor_len); +static char ** (*enchant_dict_suggest) (struct EnchantDict * dict, const char *const word, ssize_t len, size_t * out_n_suggs); +static gboolean have_enchant = FALSE; + +struct _SexySpellEntryPriv +{ + struct EnchantBroker *broker; + PangoAttrList *attr_list; + gint mark_character; + GHashTable *dict_hash; + GSList *dict_list; + gchar **words; + gint *word_starts; + gint *word_ends; + gboolean checked; +}; + +static void sexy_spell_entry_class_init(SexySpellEntryClass *klass); +static void sexy_spell_entry_editable_init (GtkEditableClass *iface); +static void sexy_spell_entry_init(SexySpellEntry *entry); +static void sexy_spell_entry_finalize(GObject *obj); +static void sexy_spell_entry_destroy(GtkObject *obj); +static gint sexy_spell_entry_expose(GtkWidget *widget, GdkEventExpose *event); +static gint sexy_spell_entry_button_press(GtkWidget *widget, GdkEventButton *event); + +/* GtkEditable handlers */ +static void sexy_spell_entry_changed(GtkEditable *editable, gpointer data); + +/* Other handlers */ +static gboolean sexy_spell_entry_popup_menu(GtkWidget *widget, SexySpellEntry *entry); + +/* Internal utility functions */ +static gint gtk_entry_find_position (GtkEntry *entry, + gint x); +static gboolean word_misspelled (SexySpellEntry *entry, + int start, + int end); +static gboolean default_word_check (SexySpellEntry *entry, + const gchar *word); +static gboolean sexy_spell_entry_activate_language_internal (SexySpellEntry *entry, + const gchar *lang, + GError **error); +static gchar *get_lang_from_dict (struct EnchantDict *dict); +static void sexy_spell_entry_recheck_all (SexySpellEntry *entry); +static void entry_strsplit_utf8 (GtkEntry *entry, + gchar ***set, + gint **starts, + gint **ends); + +static GtkEntryClass *parent_class = NULL; + +G_DEFINE_TYPE_EXTENDED(SexySpellEntry, sexy_spell_entry, GTK_TYPE_ENTRY, 0, G_IMPLEMENT_INTERFACE(GTK_TYPE_EDITABLE, sexy_spell_entry_editable_init)); + +enum +{ + WORD_CHECK, + LAST_SIGNAL +}; +static guint signals[LAST_SIGNAL] = {0}; + +static gboolean +spell_accumulator(GSignalInvocationHint *hint, GValue *return_accu, const GValue *handler_return, gpointer data) +{ + gboolean ret = g_value_get_boolean(handler_return); + /* Handlers return TRUE if the word is misspelled. In this + * case, it means that we want to stop if the word is checked + * as correct */ + g_value_set_boolean (return_accu, ret); + return ret; +} + +static void +initialize_enchant () +{ + GModule *enchant; + gpointer funcptr; + + enchant = g_module_open("libenchant", 0); + if (enchant == NULL) + { + enchant = g_module_open("libenchant.so.1", 0); + if (enchant == NULL) + return; + } + + have_enchant = TRUE; + +#define MODULE_SYMBOL(name, func) \ + g_module_symbol(enchant, (name), &funcptr); \ + (func) = funcptr; + + MODULE_SYMBOL("enchant_broker_init", enchant_broker_init) + MODULE_SYMBOL("enchant_broker_free", enchant_broker_free) + MODULE_SYMBOL("enchant_broker_free_dict", enchant_broker_free_dict) + MODULE_SYMBOL("enchant_broker_list_dicts", enchant_broker_list_dicts) + MODULE_SYMBOL("enchant_broker_request_dict", enchant_broker_request_dict) + + MODULE_SYMBOL("enchant_dict_add_to_personal", enchant_dict_add_to_personal) + MODULE_SYMBOL("enchant_dict_add_to_session", enchant_dict_add_to_session) + MODULE_SYMBOL("enchant_dict_check", enchant_dict_check) + MODULE_SYMBOL("enchant_dict_describe", enchant_dict_describe) + MODULE_SYMBOL("enchant_dict_free_suggestions", + enchant_dict_free_suggestions) + MODULE_SYMBOL("enchant_dict_store_replacement", + enchant_dict_store_replacement) + MODULE_SYMBOL("enchant_dict_suggest", enchant_dict_suggest) + +#undef MODULE_SYMBOL +} + +static void +sexy_spell_entry_class_init(SexySpellEntryClass *klass) +{ + GObjectClass *gobject_class; + GtkObjectClass *object_class; + GtkWidgetClass *widget_class; + GtkEntryClass *entry_class; + + initialize_enchant(); + + parent_class = g_type_class_peek_parent(klass); + + gobject_class = G_OBJECT_CLASS(klass); + object_class = GTK_OBJECT_CLASS(klass); + widget_class = GTK_WIDGET_CLASS(klass); + entry_class = GTK_ENTRY_CLASS(klass); + + if (have_enchant) + klass->word_check = default_word_check; + + gobject_class->finalize = sexy_spell_entry_finalize; + + object_class->destroy = sexy_spell_entry_destroy; + + widget_class->expose_event = sexy_spell_entry_expose; + widget_class->button_press_event = sexy_spell_entry_button_press; + + /** + * SexySpellEntry::word-check: + * @entry: The entry on which the signal is emitted. + * @word: The word to check. + * + * The ::word-check signal is emitted whenever the entry has to check + * a word. This allows the application to mark words as correct even + * if none of the active dictionaries contain it, such as nicknames in + * a chat client. + * + * Returns: %FALSE to indicate that the word should be marked as + * correct. + */ +/* signals[WORD_CHECK] = g_signal_new("word_check", + G_TYPE_FROM_CLASS(object_class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET(SexySpellEntryClass, word_check), + (GSignalAccumulator) spell_accumulator, NULL, + sexy_marshal_BOOLEAN__STRING, + G_TYPE_BOOLEAN, + 1, G_TYPE_STRING);*/ +} + +static void +sexy_spell_entry_editable_init (GtkEditableClass *iface) +{ +} + +static gint +gtk_entry_find_position (GtkEntry *entry, gint x) +{ + PangoLayout *layout; + PangoLayoutLine *line; + const gchar *text; + gint cursor_index; + gint index; + gint pos; + gboolean trailing; + + x = x + entry->scroll_offset; + + layout = gtk_entry_get_layout(entry); + text = pango_layout_get_text(layout); + cursor_index = g_utf8_offset_to_pointer(text, entry->current_pos) - text; + + line = pango_layout_get_lines(layout)->data; + pango_layout_line_x_to_index(line, x * PANGO_SCALE, &index, &trailing); + + if (index >= cursor_index && entry->preedit_length) { + if (index >= cursor_index + entry->preedit_length) { + index -= entry->preedit_length; + } else { + index = cursor_index; + trailing = FALSE; + } + } + + pos = g_utf8_pointer_to_offset (text, text + index); + pos += trailing; + + return pos; +} + +static void +insert_underline(SexySpellEntry *entry, guint start, guint end) +{ + PangoAttribute *ucolor = pango_attr_underline_color_new (65535, 0, 0); + PangoAttribute *unline = pango_attr_underline_new (PANGO_UNDERLINE_ERROR); + + ucolor->start_index = start; + unline->start_index = start; + + ucolor->end_index = end; + unline->end_index = end; + + pango_attr_list_insert (entry->priv->attr_list, ucolor); + pango_attr_list_insert (entry->priv->attr_list, unline); +} + +static void +get_word_extents_from_position(SexySpellEntry *entry, gint *start, gint *end, guint position) +{ + const gchar *text; + gint i, bytes_pos; + + *start = -1; + *end = -1; + + if (entry->priv->words == NULL) + return; + + text = gtk_entry_get_text(GTK_ENTRY(entry)); + bytes_pos = (gint) (g_utf8_offset_to_pointer(text, position) - text); + + for (i = 0; entry->priv->words[i]; i++) { + if (bytes_pos >= entry->priv->word_starts[i] && + bytes_pos <= entry->priv->word_ends[i]) { + *start = entry->priv->word_starts[i]; + *end = entry->priv->word_ends[i]; + return; + } + } +} + +static void +add_to_dictionary(GtkWidget *menuitem, SexySpellEntry *entry) +{ + char *word; + gint start, end; + struct EnchantDict *dict; + + if (!have_enchant) + return; + + get_word_extents_from_position(entry, &start, &end, entry->priv->mark_character); + word = gtk_editable_get_chars(GTK_EDITABLE(entry), start, end); + + dict = (struct EnchantDict *) g_object_get_data(G_OBJECT(menuitem), "enchant-dict"); + if (dict) + enchant_dict_add_to_personal(dict, word, -1); + + g_free(word); + + if (entry->priv->words) { + g_strfreev(entry->priv->words); + g_free(entry->priv->word_starts); + g_free(entry->priv->word_ends); + } + entry_strsplit_utf8(GTK_ENTRY(entry), &entry->priv->words, &entry->priv->word_starts, &entry->priv->word_ends); + sexy_spell_entry_recheck_all(entry); +} + +static void +ignore_all(GtkWidget *menuitem, SexySpellEntry *entry) +{ + char *word; + gint start, end; + GSList *li; + + if (!have_enchant) + return; + + get_word_extents_from_position(entry, &start, &end, entry->priv->mark_character); + word = gtk_editable_get_chars(GTK_EDITABLE(entry), start, end); + + for (li = entry->priv->dict_list; li; li = g_slist_next (li)) { + struct EnchantDict *dict = (struct EnchantDict *) li->data; + enchant_dict_add_to_session(dict, word, -1); + } + + g_free(word); + + if (entry->priv->words) { + g_strfreev(entry->priv->words); + g_free(entry->priv->word_starts); + g_free(entry->priv->word_ends); + } + entry_strsplit_utf8(GTK_ENTRY(entry), &entry->priv->words, &entry->priv->word_starts, &entry->priv->word_ends); + sexy_spell_entry_recheck_all(entry); +} + +static void +replace_word(GtkWidget *menuitem, SexySpellEntry *entry) +{ + char *oldword; + const char *newword; + gint start, end; + gint cursor; + struct EnchantDict *dict; + + if (!have_enchant) + return; + + get_word_extents_from_position(entry, &start, &end, entry->priv->mark_character); + oldword = gtk_editable_get_chars(GTK_EDITABLE(entry), start, end); + newword = gtk_label_get_text(GTK_LABEL(GTK_BIN(menuitem)->child)); + + cursor = gtk_editable_get_position(GTK_EDITABLE(entry)); + /* is the cursor at the end? If so, restore it there */ + if (g_utf8_strlen(gtk_entry_get_text(GTK_ENTRY(entry)), -1) == cursor) + cursor = -1; + else if(cursor > start && cursor <= end) + cursor = start; + + gtk_editable_delete_text(GTK_EDITABLE(entry), start, end); + gtk_editable_set_position(GTK_EDITABLE(entry), start); + gtk_editable_insert_text(GTK_EDITABLE(entry), newword, strlen(newword), + &start); + gtk_editable_set_position(GTK_EDITABLE(entry), cursor); + + dict = (struct EnchantDict *) g_object_get_data(G_OBJECT(menuitem), "enchant-dict"); + + if (dict) + enchant_dict_store_replacement(dict, + oldword, -1, + newword, -1); + + g_free(oldword); +} + +static void +build_suggestion_menu(SexySpellEntry *entry, GtkWidget *menu, struct EnchantDict *dict, const gchar *word) +{ + GtkWidget *mi; + gchar **suggestions; + size_t n_suggestions, i; + + if (!have_enchant) + return; + + suggestions = enchant_dict_suggest(dict, word, -1, &n_suggestions); + + if (suggestions == NULL || n_suggestions == 0) { + /* no suggestions. put something in the menu anyway... */ + GtkWidget *label = gtk_label_new(""); + gtk_label_set_markup(GTK_LABEL(label), _("<i>(no suggestions)</i>")); + + mi = gtk_separator_menu_item_new(); + gtk_container_add(GTK_CONTAINER(mi), label); + gtk_widget_show_all(mi); + gtk_menu_shell_prepend(GTK_MENU_SHELL(menu), mi); + } else { + /* build a set of menus with suggestions */ + for (i = 0; i < n_suggestions; i++) { + if ((i != 0) && (i % 10 == 0)) { + mi = gtk_separator_menu_item_new(); + gtk_widget_show(mi); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), mi); + + mi = gtk_menu_item_new_with_label(_("More...")); + gtk_widget_show(mi); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), mi); + + menu = gtk_menu_new(); + gtk_menu_item_set_submenu(GTK_MENU_ITEM(mi), menu); + } + + mi = gtk_menu_item_new_with_label(suggestions[i]); + g_object_set_data(G_OBJECT(mi), "enchant-dict", dict); + g_signal_connect(G_OBJECT(mi), "activate", G_CALLBACK(replace_word), entry); + gtk_widget_show(mi); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), mi); + } + } + + enchant_dict_free_suggestions(dict, suggestions); +} + +static GtkWidget * +build_spelling_menu(SexySpellEntry *entry, const gchar *word) +{ + struct EnchantDict *dict; + GtkWidget *topmenu, *mi; + gchar *label; + + if (!have_enchant) + return NULL; + + topmenu = gtk_menu_new(); + + if (entry->priv->dict_list == NULL) + return topmenu; + +#if 1 + dict = (struct EnchantDict *) entry->priv->dict_list->data; + build_suggestion_menu(entry, topmenu, dict, word); +#else + /* Suggestions */ + if (g_slist_length(entry->priv->dict_list) == 1) { + dict = (struct EnchantDict *) entry->priv->dict_list->data; + build_suggestion_menu(entry, topmenu, dict, word); + } else { + GSList *li; + GtkWidget *menu; + gchar *lang, *lang_name; + + for (li = entry->priv->dict_list; li; li = g_slist_next (li)) { + dict = (struct EnchantDict *) li->data; + lang = get_lang_from_dict(dict); + lang_name = gtkspell_iso_codes_lookup_name_for_code(lang); + if (lang_name) { + mi = gtk_menu_item_new_with_label(lang_name); + g_free(lang_name); + } else { + mi = gtk_menu_item_new_with_label(lang); + } + g_free(lang); + + gtk_widget_show(mi); + gtk_menu_shell_append(GTK_MENU_SHELL(topmenu), mi); + menu = gtk_menu_new(); + gtk_menu_item_set_submenu(GTK_MENU_ITEM(mi), menu); + build_suggestion_menu(entry, menu, dict, word); + } + } +#endif + + /* Separator */ + mi = gtk_separator_menu_item_new (); + gtk_widget_show(mi); + gtk_menu_shell_append(GTK_MENU_SHELL(topmenu), mi); + + /* + 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)); + +#if 1 + dict = (struct EnchantDict *) entry->priv->dict_list->data; + g_object_set_data(G_OBJECT(mi), "enchant-dict", dict); + g_signal_connect(G_OBJECT(mi), "activate", G_CALLBACK(add_to_dictionary), entry); +#else + if (g_slist_length(entry->priv->dict_list) == 1) { + dict = (struct EnchantDict *) entry->priv->dict_list->data; + g_object_set_data(G_OBJECT(mi), "enchant-dict", dict); + g_signal_connect(G_OBJECT(mi), "activate", G_CALLBACK(add_to_dictionary), entry); + } else { + GSList *li; + GtkWidget *menu, *submi; + gchar *lang, *lang_name; + + menu = gtk_menu_new(); + gtk_menu_item_set_submenu(GTK_MENU_ITEM(mi), menu); + + for (li = entry->priv->dict_list; li; li = g_slist_next(li)) { + dict = (struct EnchantDict *)li->data; + lang = get_lang_from_dict(dict); + lang_name = gtkspell_iso_codes_lookup_name_for_code(lang); + if (lang_name) { + submi = gtk_menu_item_new_with_label(lang_name); + g_free(lang_name); + } else { + submi = gtk_menu_item_new_with_label(lang); + } + g_free(lang); + g_object_set_data(G_OBJECT(submi), "enchant-dict", dict); + + g_signal_connect(G_OBJECT(submi), "activate", G_CALLBACK(add_to_dictionary), entry); + + gtk_widget_show(submi); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), submi); + } + } +#endif + + gtk_widget_show_all(mi); + gtk_menu_shell_append(GTK_MENU_SHELL(topmenu), mi); + + /* - 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), entry); + gtk_widget_show_all(mi); + gtk_menu_shell_append(GTK_MENU_SHELL(topmenu), mi); + + return topmenu; +} + +static void +sexy_spell_entry_populate_popup(SexySpellEntry *entry, GtkMenu *menu, gpointer data) +{ + GtkWidget *icon, *mi; + gint start, end; + gchar *word; + + if ((have_enchant == FALSE) || (entry->priv->checked == FALSE)) + return; + + if (g_slist_length(entry->priv->dict_list) == 0) + return; + + get_word_extents_from_position(entry, &start, &end, entry->priv->mark_character); + if (start == end) + return; + if (!word_misspelled(entry, start, end)) + return; + + /* separator */ + mi = gtk_separator_menu_item_new(); + gtk_widget_show(mi); + gtk_menu_shell_prepend(GTK_MENU_SHELL(menu), mi); + + /* Above the separator, show the suggestions menu */ + icon = gtk_image_new_from_stock(GTK_STOCK_SPELL_CHECK, GTK_ICON_SIZE_MENU); + mi = gtk_image_menu_item_new_with_label(_("Spelling Suggestions")); + gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(mi), icon); + + word = gtk_editable_get_chars(GTK_EDITABLE(entry), start, end); + g_assert(word != NULL); + gtk_menu_item_set_submenu(GTK_MENU_ITEM(mi), build_spelling_menu(entry, word)); + g_free(word); + + gtk_widget_show_all(mi); + gtk_menu_shell_prepend(GTK_MENU_SHELL(menu), mi); +} + +static void +sexy_spell_entry_init(SexySpellEntry *entry) +{ + entry->priv = g_new0(SexySpellEntryPriv, 1); + + entry->priv->dict_hash = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); + + if (have_enchant) + sexy_spell_entry_activate_default_languages(entry); + + entry->priv->attr_list = pango_attr_list_new(); + + entry->priv->checked = TRUE; + + g_signal_connect(G_OBJECT(entry), "popup-menu", G_CALLBACK(sexy_spell_entry_popup_menu), entry); + g_signal_connect(G_OBJECT(entry), "populate-popup", G_CALLBACK(sexy_spell_entry_populate_popup), NULL); + g_signal_connect(G_OBJECT(entry), "changed", G_CALLBACK(sexy_spell_entry_changed), NULL); +} + +static void +sexy_spell_entry_finalize(GObject *obj) +{ + SexySpellEntry *entry; + + g_return_if_fail(obj != NULL); + g_return_if_fail(SEXY_IS_SPELL_ENTRY(obj)); + + entry = SEXY_SPELL_ENTRY(obj); + + if (entry->priv->attr_list) + pango_attr_list_unref(entry->priv->attr_list); + if (entry->priv->dict_hash) + g_hash_table_destroy(entry->priv->dict_hash); + if (entry->priv->words) + g_strfreev(entry->priv->words); + if (entry->priv->word_starts) + g_free(entry->priv->word_starts); + if (entry->priv->word_ends) + g_free(entry->priv->word_ends); + + if (have_enchant) { + if (entry->priv->broker) { + GSList *li; + for (li = entry->priv->dict_list; li; li = g_slist_next(li)) { + struct EnchantDict *dict = (struct EnchantDict*) li->data; + enchant_broker_free_dict (entry->priv->broker, dict); + } + g_slist_free (entry->priv->dict_list); + + enchant_broker_free(entry->priv->broker); + } + } + + g_free(entry->priv); + + if (G_OBJECT_CLASS(parent_class)->finalize) + G_OBJECT_CLASS(parent_class)->finalize(obj); +} + +static void +sexy_spell_entry_destroy(GtkObject *obj) +{ + SexySpellEntry *entry; + + entry = SEXY_SPELL_ENTRY(obj); + + if (GTK_OBJECT_CLASS(parent_class)->destroy) + GTK_OBJECT_CLASS(parent_class)->destroy(obj); +} + +/** + * sexy_spell_entry_new + * + * Creates a new SexySpellEntry widget. + * + * Returns: a new #SexySpellEntry. + */ +GtkWidget * +sexy_spell_entry_new(void) +{ + return GTK_WIDGET(g_object_new(SEXY_TYPE_SPELL_ENTRY, NULL)); +} + +GQuark +sexy_spell_error_quark(void) +{ + static GQuark q = 0; + if (q == 0) + q = g_quark_from_static_string("sexy-spell-error-quark"); + return q; +} + +static gboolean +default_word_check(SexySpellEntry *entry, const gchar *word) +{ + gboolean result = TRUE; + GSList *li; + + if (!have_enchant) + return result; + + if (g_unichar_isalpha(*word) == FALSE) { + /* We only want to check words */ + return FALSE; + } + for (li = entry->priv->dict_list; li; li = g_slist_next (li)) { + struct EnchantDict *dict = (struct EnchantDict *) li->data; + if (enchant_dict_check(dict, word, strlen(word)) == 0) { + result = FALSE; + break; + } + } + return result; +} + +static gboolean +word_misspelled(SexySpellEntry *entry, int start, int end) +{ + const gchar *text; + gchar *word; + gboolean ret; + + if (start == end) + return FALSE; + text = gtk_entry_get_text(GTK_ENTRY(entry)); + word = g_new0(gchar, end - start + 2); + + g_strlcpy(word, text + start, end - start + 1); + +#if 0 + g_signal_emit(entry, signals[WORD_CHECK], 0, word, &ret); +#else + ret = default_word_check (entry, word); +#endif + + g_free(word); + return ret; +} + +static void +check_word(SexySpellEntry *entry, int start, int end) +{ + PangoAttrIterator *it; + + /* Check to see if we've got any attributes at this position. + * If so, free them, since we'll readd it if the word is misspelled */ + it = pango_attr_list_get_iterator(entry->priv->attr_list); + if (it == NULL) + return; + do { + gint s, e; + pango_attr_iterator_range(it, &s, &e); + if (s == start) { + GSList *attrs = pango_attr_iterator_get_attrs(it); + g_slist_foreach(attrs, (GFunc) pango_attribute_destroy, NULL); + g_slist_free(attrs); + } + } while (pango_attr_iterator_next(it)); + pango_attr_iterator_destroy(it); + + if (word_misspelled(entry, start, end)) + insert_underline(entry, start, end); +} + +static void +sexy_spell_entry_recheck_all(SexySpellEntry *entry) +{ + GdkRectangle rect; + GtkWidget *widget = GTK_WIDGET(entry); + PangoLayout *layout; + int length, i; + + if ((have_enchant == FALSE) || (entry->priv->checked == FALSE)) + return; + + if (g_slist_length(entry->priv->dict_list) == 0) + return; + + /* Remove all existing pango attributes. These will get readded as we check */ + pango_attr_list_unref(entry->priv->attr_list); + entry->priv->attr_list = pango_attr_list_new(); + + /* Loop through words */ + for (i = 0; entry->priv->words[i]; i++) { + length = strlen(entry->priv->words[i]); + if (length == 0) + continue; + check_word(entry, entry->priv->word_starts[i], entry->priv->word_ends[i]); + } + + layout = gtk_entry_get_layout(GTK_ENTRY(entry)); + pango_layout_set_attributes(layout, entry->priv->attr_list); + + if (GTK_WIDGET_REALIZED(GTK_WIDGET(entry))) { + rect.x = 0; rect.y = 0; + rect.width = widget->allocation.width; + rect.height = widget->allocation.height; + gdk_window_invalidate_rect(widget->window, &rect, TRUE); + } +} + +static gint +sexy_spell_entry_expose(GtkWidget *widget, GdkEventExpose *event) +{ + SexySpellEntry *entry = SEXY_SPELL_ENTRY(widget); + GtkEntry *gtk_entry = GTK_ENTRY(widget); + PangoLayout *layout; + + if (entry->priv->checked) { + layout = gtk_entry_get_layout(gtk_entry); + pango_layout_set_attributes(layout, entry->priv->attr_list); + } + + return GTK_WIDGET_CLASS(parent_class)->expose_event (widget, event); +} + +static gint +sexy_spell_entry_button_press(GtkWidget *widget, GdkEventButton *event) +{ + SexySpellEntry *entry = SEXY_SPELL_ENTRY(widget); + GtkEntry *gtk_entry = GTK_ENTRY(widget); + gint pos; + + pos = gtk_entry_find_position(gtk_entry, event->x); + entry->priv->mark_character = pos; + + return GTK_WIDGET_CLASS(parent_class)->button_press_event (widget, event); +} + +static gboolean +sexy_spell_entry_popup_menu(GtkWidget *widget, SexySpellEntry *entry) +{ + /* Menu popped up from a keybinding (menu key or <shift>+F10). Use + * the cursor position as the mark position */ + entry->priv->mark_character = gtk_editable_get_position (GTK_EDITABLE (entry)); + return FALSE; +} + +static void +entry_strsplit_utf8(GtkEntry *entry, gchar ***set, gint **starts, gint **ends) +{ + PangoLayout *layout; + PangoLogAttr *log_attrs; + const gchar *text; + gint n_attrs, n_strings, i, j; + + layout = gtk_entry_get_layout(GTK_ENTRY(entry)); + text = gtk_entry_get_text(GTK_ENTRY(entry)); + pango_layout_get_log_attrs(layout, &log_attrs, &n_attrs); + + /* Find how many words we have */ + n_strings = 0; + for (i = 0; i < n_attrs; i++) + if (log_attrs[i].is_word_start) + n_strings++; + + *set = g_new0(gchar *, n_strings + 1); + *starts = g_new0(gint, n_strings); + *ends = g_new0(gint, n_strings); + + /* Copy out strings */ + for (i = 0, j = 0; i < n_attrs; i++) { + if (log_attrs[i].is_word_start) { + gint cend, bytes; + gchar *start; + + /* Find the end of this string */ + cend = i; + while (!(log_attrs[cend].is_word_end)) + cend++; + + /* Copy sub-string */ + start = g_utf8_offset_to_pointer(text, i); + bytes = (gint) (g_utf8_offset_to_pointer(text, cend) - start); + (*set)[j] = g_new0(gchar, bytes + 1); + (*starts)[j] = (gint) (start - text); + (*ends)[j] = (gint) (start - text + bytes); + g_utf8_strncpy((*set)[j], start, cend - i); + + /* Move on to the next word */ + j++; + } + } + + g_free (log_attrs); +} + +static void +sexy_spell_entry_changed(GtkEditable *editable, gpointer data) +{ + SexySpellEntry *entry = SEXY_SPELL_ENTRY(editable); + if (entry->priv->checked == FALSE) + return; + if (g_slist_length(entry->priv->dict_list) == 0) + return; + + if (entry->priv->words) { + g_strfreev(entry->priv->words); + g_free(entry->priv->word_starts); + g_free(entry->priv->word_ends); + } + entry_strsplit_utf8(GTK_ENTRY(entry), &entry->priv->words, &entry->priv->word_starts, &entry->priv->word_ends); + sexy_spell_entry_recheck_all(entry); +} + +static gboolean +enchant_has_lang(const gchar *lang, GSList *langs) { + GSList *i; + for (i = langs; i; i = g_slist_next(i)) { + if (strcmp(lang, i->data) == 0) { + return TRUE; + } + } + return FALSE; +} + +/** + * sexy_spell_entry_activate_default_languages: + * @entry: A #SexySpellEntry. + * + * Activate spell checking for languages specified in the $LANG + * or $LANGUAGE environment variables. These languages are + * activated by default, so this function need only be called + * if they were previously deactivated. + */ +void +sexy_spell_entry_activate_default_languages(SexySpellEntry *entry) +{ +#if GLIB_CHECK_VERSION (2, 6, 0) + const gchar* const *langs; + int i; + gchar *lastprefix = NULL; + GSList *enchant_langs; + + if (!have_enchant) + return; + + if (!entry->priv->broker) + entry->priv->broker = enchant_broker_init(); + + + langs = g_get_language_names (); + + if (langs == NULL) + return; + + enchant_langs = sexy_spell_entry_get_languages(entry); + + for (i = 0; langs[i]; i++) { + if ((g_strncasecmp(langs[i], "C", 1) != 0) && + (strlen(langs[i]) >= 2) && + enchant_has_lang(langs[i], enchant_langs)) { + if ((lastprefix == NULL) || (g_str_has_prefix(langs[i], lastprefix) == FALSE)) + sexy_spell_entry_activate_language_internal(entry, langs[i], NULL); + if (lastprefix != NULL) + g_free(lastprefix); + lastprefix = g_strndup(langs[i], 2); + } + } + if (lastprefix != NULL) + g_free(lastprefix); + + g_slist_foreach(enchant_langs, (GFunc) g_free, NULL); + g_slist_free(enchant_langs); + + /* If we don't have any languages activated, use "en" */ + if (entry->priv->dict_list == NULL) + sexy_spell_entry_activate_language_internal(entry, "en", NULL); +#else + gchar *lang; + + if (!have_enchant) + return; + + lang = (gchar *) g_getenv("LANG"); + + if (lang != NULL) { + if (g_strncasecmp(lang, "C", 1) == 0) + lang = NULL; + else if (lang[0] == '\0') + lang = NULL; + } + + if (lang == NULL) + lang = "en"; + + sexy_spell_entry_activate_language_internal(entry, lang, NULL); +#endif +} + +static void +get_lang_from_dict_cb(const char * const lang_tag, + const char * const provider_name, + const char * const provider_desc, + const char * const provider_file, + void * user_data) { + gchar **lang = (gchar **)user_data; + *lang = g_strdup(lang_tag); +} + +static gchar * +get_lang_from_dict(struct EnchantDict *dict) +{ + gchar *lang; + + if (!have_enchant) + return NULL; + + enchant_dict_describe(dict, get_lang_from_dict_cb, &lang); + return lang; +} + +static gboolean +sexy_spell_entry_activate_language_internal(SexySpellEntry *entry, const gchar *lang, GError **error) +{ + struct EnchantDict *dict; + + if (!have_enchant) + return FALSE; + + if (!entry->priv->broker) + entry->priv->broker = enchant_broker_init(); + + if (g_hash_table_lookup(entry->priv->dict_hash, lang)) + return TRUE; + + dict = enchant_broker_request_dict(entry->priv->broker, lang); + + if (!dict) { + g_set_error(error, SEXY_SPELL_ERROR, SEXY_SPELL_ERROR_BACKEND, _("enchant error for language: %s"), lang); + return FALSE; + } + + entry->priv->dict_list = g_slist_append(entry->priv->dict_list, (gpointer) dict); + g_hash_table_insert(entry->priv->dict_hash, get_lang_from_dict(dict), (gpointer) dict); + + return TRUE; +} + +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) +{ + GSList **langs = (GSList **)user_data; + + *langs = g_slist_append(*langs, (gpointer)g_strdup(lang_tag)); +} + +/** + * sexy_spell_entry_get_languages: + * @entry: A #SexySpellEntry. + * + * Retrieve a list of language codes for which dictionaries are available. + * + * Returns: a new #GList object, or %NULL on error. + */ +GSList * +sexy_spell_entry_get_languages(const SexySpellEntry *entry) +{ + GSList *langs = NULL; + + g_return_val_if_fail(entry != NULL, NULL); + g_return_val_if_fail(SEXY_IS_SPELL_ENTRY(entry), NULL); + + if (enchant_broker_list_dicts == NULL) + return NULL; + + if (!entry->priv->broker) + return NULL; + + enchant_broker_list_dicts(entry->priv->broker, dict_describe_cb, &langs); + + return langs; +} + +/** + * sexy_spell_entry_get_language_name: + * @entry: A #SexySpellEntry. + * @lang: The language code to lookup a friendly name for. + * + * Get a friendly name for a given locale. + * + * Returns: The name of the locale. Should be freed with g_free() + */ +gchar * +sexy_spell_entry_get_language_name(const SexySpellEntry *entry, + const gchar *lang) +{ + /*if (have_enchant) + return gtkspell_iso_codes_lookup_name_for_code(lang);*/ + return NULL; +} + +/** + * sexy_spell_entry_language_is_active: + * @entry: A #SexySpellEntry. + * @lang: The language to use, in a form enchant understands. + * + * Determine if a given language is currently active. + * + * Returns: TRUE if the language is active. + */ +gboolean +sexy_spell_entry_language_is_active(const SexySpellEntry *entry, + const gchar *lang) +{ + return (g_hash_table_lookup(entry->priv->dict_hash, lang) != NULL); +} + +/** + * sexy_spell_entry_activate_language: + * @entry: A #SexySpellEntry + * @lang: The language to use in a form Enchant understands. Typically either + * a two letter language code or a locale code in the form xx_XX. + * @error: Return location for error. + * + * Activate spell checking for the language specifed. + * + * Returns: FALSE if there was an error. + */ +gboolean +sexy_spell_entry_activate_language(SexySpellEntry *entry, const gchar *lang, GError **error) +{ + gboolean ret; + + g_return_val_if_fail(entry != NULL, FALSE); + g_return_val_if_fail(SEXY_IS_SPELL_ENTRY(entry), FALSE); + g_return_val_if_fail(lang != NULL && lang != '\0', FALSE); + + if (!have_enchant) + return FALSE; + + if (error) + g_return_val_if_fail(*error == NULL, FALSE); + + ret = sexy_spell_entry_activate_language_internal(entry, lang, error); + + if (ret) { + if (entry->priv->words) { + g_strfreev(entry->priv->words); + g_free(entry->priv->word_starts); + g_free(entry->priv->word_ends); + } + entry_strsplit_utf8(GTK_ENTRY(entry), &entry->priv->words, &entry->priv->word_starts, &entry->priv->word_ends); + sexy_spell_entry_recheck_all(entry); + } + + return ret; +} + +/** + * sexy_spell_entry_deactivate_language: + * @entry: A #SexySpellEntry. + * @lang: The language in a form Enchant understands. Typically either + * a two letter language code or a locale code in the form xx_XX. + * + * Deactivate spell checking for the language specifed. + */ +void +sexy_spell_entry_deactivate_language(SexySpellEntry *entry, const gchar *lang) +{ + g_return_if_fail(entry != NULL); + g_return_if_fail(SEXY_IS_SPELL_ENTRY(entry)); + + if (!have_enchant) + return; + + if (!entry->priv->dict_list) + return; + + if (lang) { + struct EnchantDict *dict; + + dict = g_hash_table_lookup(entry->priv->dict_hash, lang); + if (!dict) + return; + enchant_broker_free_dict(entry->priv->broker, dict); + entry->priv->dict_list = g_slist_remove(entry->priv->dict_list, dict); + g_hash_table_remove (entry->priv->dict_hash, lang); + } else { + /* deactivate all */ + GSList *li; + struct EnchantDict *dict; + + for (li = entry->priv->dict_list; li; li = g_slist_next(li)) { + dict = (struct EnchantDict *)li->data; + enchant_broker_free_dict(entry->priv->broker, dict); + } + + g_slist_free (entry->priv->dict_list); + g_hash_table_destroy (entry->priv->dict_hash); + entry->priv->dict_hash = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); + entry->priv->dict_list = NULL; + } + + if (entry->priv->words) { + g_strfreev(entry->priv->words); + g_free(entry->priv->word_starts); + g_free(entry->priv->word_ends); + } + entry_strsplit_utf8(GTK_ENTRY(entry), &entry->priv->words, &entry->priv->word_starts, &entry->priv->word_ends); + sexy_spell_entry_recheck_all(entry); +} + +/** + * sexy_spell_entry_set_active_languages: + * @entry: A #SexySpellEntry + * @langs: A list of language codes to activate, in a form Enchant understands. + * Typically either a two letter language code or a locale code in the + * form xx_XX. + * @error: Return location for error. + * + * Activate spell checking for only the languages specified. + * + * Returns: FALSE if there was an error. + */ +gboolean +sexy_spell_entry_set_active_languages(SexySpellEntry *entry, GSList *langs, GError **error) +{ + GSList *li; + + g_return_val_if_fail(entry != NULL, FALSE); + g_return_val_if_fail(SEXY_IS_SPELL_ENTRY(entry), FALSE); + g_return_val_if_fail(langs != NULL, FALSE); + + if (!have_enchant) + return FALSE; + + /* deactivate all languages first */ + sexy_spell_entry_deactivate_language(entry, NULL); + + for (li = langs; li; li = g_slist_next(li)) { + if (sexy_spell_entry_activate_language_internal(entry, + (const gchar *) li->data, error) == FALSE) + return FALSE; + } + if (entry->priv->words) { + g_strfreev(entry->priv->words); + g_free(entry->priv->word_starts); + g_free(entry->priv->word_ends); + } + entry_strsplit_utf8(GTK_ENTRY(entry), &entry->priv->words, &entry->priv->word_starts, &entry->priv->word_ends); + sexy_spell_entry_recheck_all(entry); + return TRUE; +} + +/** + * sexy_spell_entry_get_active_languages: + * @entry: A #SexySpellEntry + * + * Retrieve a list of the currently active languages. + * + * Returns: A GSList of char* values with language codes (en, fr, etc). Both + * the data and the list must be freed by the user. + */ +GSList * +sexy_spell_entry_get_active_languages(SexySpellEntry *entry) +{ + GSList *ret = NULL, *li; + struct EnchantDict *dict; + gchar *lang; + + g_return_val_if_fail(entry != NULL, NULL); + g_return_val_if_fail(SEXY_IS_SPELL_ENTRY(entry), NULL); + + if (!have_enchant) + return NULL; + + for (li = entry->priv->dict_list; li; li = g_slist_next(li)) { + dict = (struct EnchantDict *) li->data; + lang = get_lang_from_dict(dict); + ret = g_slist_append(ret, lang); + } + return ret; +} + +/** + * sexy_spell_entry_is_checked: + * @entry: A #SexySpellEntry. + * + * Queries a #SexySpellEntry and returns whether the entry has spell-checking enabled. + * + * Returns: TRUE if the entry has spell-checking enabled. + */ +gboolean +sexy_spell_entry_is_checked(SexySpellEntry *entry) +{ + return entry->priv->checked; +} + +/** + * sexy_spell_entry_set_checked: + * @entry: A #SexySpellEntry. + * @checked: Whether to enable spell-checking + * + * Sets whether the entry has spell-checking enabled. + */ +void +sexy_spell_entry_set_checked(SexySpellEntry *entry, gboolean checked) +{ + GtkWidget *widget; + + if (entry->priv->checked == checked) + return; + + entry->priv->checked = checked; + widget = GTK_WIDGET(entry); + + if (checked == FALSE && GTK_WIDGET_REALIZED(widget)) { + PangoLayout *layout; + GdkRectangle rect; + + pango_attr_list_unref(entry->priv->attr_list); + entry->priv->attr_list = pango_attr_list_new(); + + layout = gtk_entry_get_layout(GTK_ENTRY(entry)); + pango_layout_set_attributes(layout, entry->priv->attr_list); + + rect.x = 0; rect.y = 0; + rect.width = widget->allocation.width; + rect.height = widget->allocation.height; + gdk_window_invalidate_rect(widget->window, &rect, TRUE); + } else { + if (entry->priv->words) { + g_strfreev(entry->priv->words); + g_free(entry->priv->word_starts); + g_free(entry->priv->word_ends); + } + entry_strsplit_utf8(GTK_ENTRY(entry), &entry->priv->words, &entry->priv->word_starts, &entry->priv->word_ends); + sexy_spell_entry_recheck_all(entry); + } +} |