1626 lines
44 KiB
C
1626 lines
44 KiB
C
/*
|
|
* @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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
|
|
*/
|
|
#ifdef HAVE_CONFIG_H
|
|
#include "config.h"
|
|
#endif
|
|
|
|
#include <gtk/gtk.h>
|
|
#include "sexy-spell-entry.h"
|
|
#include <string.h>
|
|
#include <fcntl.h>
|
|
#include <stdlib.h>
|
|
#include <glib/gi18n.h>
|
|
#include <sys/types.h>
|
|
#include <sys/stat.h>
|
|
#include "sexy-iso-codes.h"
|
|
#include "../common/marshal.h"
|
|
|
|
#ifdef WIN32
|
|
#include "../common/typedef.h"
|
|
#include <io.h>
|
|
#else
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
#include "../common/cfgfiles.h"
|
|
#include "../common/hexchatc.h"
|
|
#include "palette.h"
|
|
#include "xtext.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;
|
|
gboolean parseattr;
|
|
};
|
|
|
|
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(GObject *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;
|
|
|
|
#ifdef HAVE_ISO_CODES
|
|
static int codetable_ref = 0;
|
|
#endif
|
|
|
|
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;
|
|
|
|
#ifdef WIN32
|
|
enchant = g_module_open("libenchant.dll", 0);
|
|
#else
|
|
enchant = g_module_open("libenchant", 0);
|
|
#endif
|
|
if (enchant == NULL)
|
|
{
|
|
#ifndef WIN32
|
|
enchant = g_module_open("libenchant.so.1", 0);
|
|
if (enchant == NULL)
|
|
{
|
|
#ifdef __APPLE__
|
|
enchant = g_module_open("libenchant.dylib", 0);
|
|
if (enchant == NULL)
|
|
#endif
|
|
return;
|
|
}
|
|
#else
|
|
return;
|
|
#endif
|
|
}
|
|
|
|
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;
|
|
GObjectClass *object_class;
|
|
GtkWidgetClass *widget_class;
|
|
|
|
initialize_enchant();
|
|
|
|
parent_class = g_type_class_peek_parent(klass);
|
|
|
|
gobject_class = G_OBJECT_CLASS(klass);
|
|
object_class = G_OBJECT_CLASS(klass);
|
|
widget_class = GTK_WIDGET_CLASS(klass);
|
|
|
|
if (have_enchant)
|
|
klass->word_check = default_word_check;
|
|
|
|
gobject_class->finalize = sexy_spell_entry_finalize;
|
|
|
|
object_class->dispose = 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,
|
|
_hexchat_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_hiddenchar (SexySpellEntry *entry, guint start, guint end)
|
|
{
|
|
/* FIXME: Pango does not properly reflect the new widths after a char
|
|
* is 'hidden' */
|
|
#if 0
|
|
PangoAttribute *hattr;
|
|
PangoRectangle *rect = g_malloc (sizeof (PangoRectangle));
|
|
|
|
rect->x = 0;
|
|
rect->y = 0;
|
|
rect->width = 0;
|
|
rect->height = 0;
|
|
|
|
hattr = pango_attr_shape_new (rect, rect);
|
|
hattr->start_index = start;
|
|
hattr->end_index = end;
|
|
pango_attr_list_insert (entry->priv->attr_list, hattr);
|
|
|
|
g_free (rect);
|
|
#endif
|
|
}
|
|
|
|
static void
|
|
insert_underline_error (SexySpellEntry *entry, guint start, guint end)
|
|
{
|
|
PangoAttribute *ucolor;
|
|
PangoAttribute *unline;
|
|
|
|
ucolor = pango_attr_underline_color_new (colors[COL_SPELL].red, colors[COL_SPELL].green, colors[COL_SPELL].blue);
|
|
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
|
|
insert_underline (SexySpellEntry *entry, guint start, gboolean toggle)
|
|
{
|
|
PangoAttribute *uattr;
|
|
|
|
uattr = pango_attr_underline_new (toggle ? PANGO_UNDERLINE_NONE : PANGO_UNDERLINE_SINGLE);
|
|
uattr->start_index = start;
|
|
uattr->end_index = PANGO_ATTR_INDEX_TO_TEXT_END;
|
|
pango_attr_list_change (entry->priv->attr_list, uattr);
|
|
}
|
|
|
|
static void
|
|
insert_bold (SexySpellEntry *entry, guint start, gboolean toggle)
|
|
{
|
|
PangoAttribute *battr;
|
|
|
|
battr = pango_attr_weight_new (toggle ? PANGO_WEIGHT_NORMAL : PANGO_WEIGHT_BOLD);
|
|
battr->start_index = start;
|
|
battr->end_index = PANGO_ATTR_INDEX_TO_TEXT_END;
|
|
pango_attr_list_change (entry->priv->attr_list, battr);
|
|
}
|
|
|
|
static void
|
|
insert_italic (SexySpellEntry *entry, guint start, gboolean toggle)
|
|
{
|
|
PangoAttribute *iattr;
|
|
|
|
iattr = pango_attr_style_new (toggle ? PANGO_STYLE_NORMAL : PANGO_STYLE_ITALIC);
|
|
iattr->start_index = start;
|
|
iattr->end_index = PANGO_ATTR_INDEX_TO_TEXT_END;
|
|
pango_attr_list_change (entry->priv->attr_list, iattr);
|
|
}
|
|
|
|
static void
|
|
insert_color (SexySpellEntry *entry, guint start, int fgcolor, int bgcolor)
|
|
{
|
|
PangoAttribute *fgattr;
|
|
PangoAttribute *ulattr;
|
|
PangoAttribute *bgattr;
|
|
|
|
if (fgcolor < 0 || fgcolor > MAX_COL)
|
|
{
|
|
fgattr = pango_attr_foreground_new (colors[COL_FG].red, colors[COL_FG].green, colors[COL_FG].blue);
|
|
ulattr = pango_attr_underline_color_new (colors[COL_FG].red, colors[COL_FG].green, colors[COL_FG].blue);
|
|
}
|
|
else
|
|
{
|
|
fgattr = pango_attr_foreground_new (colors[fgcolor].red, colors[fgcolor].green, colors[fgcolor].blue);
|
|
ulattr = pango_attr_underline_color_new (colors[fgcolor].red, colors[fgcolor].green, colors[fgcolor].blue);
|
|
}
|
|
|
|
if (bgcolor < 0 || bgcolor > MAX_COL)
|
|
bgattr = pango_attr_background_new (colors[COL_BG].red, colors[COL_BG].green, colors[COL_BG].blue);
|
|
else
|
|
bgattr = pango_attr_background_new (colors[bgcolor].red, colors[bgcolor].green, colors[bgcolor].blue);
|
|
|
|
fgattr->start_index = start;
|
|
fgattr->end_index = PANGO_ATTR_INDEX_TO_TEXT_END;
|
|
pango_attr_list_change (entry->priv->attr_list, fgattr);
|
|
ulattr->start_index = start;
|
|
ulattr->end_index = PANGO_ATTR_INDEX_TO_TEXT_END;
|
|
pango_attr_list_change (entry->priv->attr_list, ulattr);
|
|
bgattr->start_index = start;
|
|
bgattr->end_index = PANGO_ATTR_INDEX_TO_TEXT_END;
|
|
pango_attr_list_change (entry->priv->attr_list, bgattr);
|
|
}
|
|
|
|
static void
|
|
insert_reset (SexySpellEntry *entry, guint start)
|
|
{
|
|
insert_bold (entry, start, TRUE);
|
|
insert_underline (entry, start, TRUE);
|
|
insert_italic (entry, start, TRUE);
|
|
insert_color (entry, start, -1, -1);
|
|
}
|
|
|
|
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_get_child (GTK_BIN(menuitem))));
|
|
|
|
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;
|
|
|
|
/* 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 = sexy_spell_entry_get_language_name (entry, 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);
|
|
}
|
|
}
|
|
|
|
/* 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 (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 = sexy_spell_entry_get_language_name (entry, 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);
|
|
}
|
|
}
|
|
|
|
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)
|
|
{
|
|
#ifdef HAVE_ISO_CODES
|
|
if (codetable_ref == 0)
|
|
codetable_init ();
|
|
codetable_ref++;
|
|
#endif
|
|
sexy_spell_entry_activate_default_languages(entry);
|
|
}
|
|
|
|
entry->priv->attr_list = pango_attr_list_new();
|
|
|
|
entry->priv->checked = TRUE;
|
|
entry->priv->parseattr = 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);
|
|
#ifdef HAVE_ISO_CODES
|
|
codetable_ref--;
|
|
if (codetable_ref == 0)
|
|
codetable_free ();
|
|
#endif
|
|
|
|
if (G_OBJECT_CLASS(parent_class)->finalize)
|
|
G_OBJECT_CLASS(parent_class)->finalize(obj);
|
|
}
|
|
|
|
static void
|
|
sexy_spell_entry_destroy(GObject *obj)
|
|
{
|
|
if (G_OBJECT_CLASS(parent_class)->dispose)
|
|
G_OBJECT_CLASS(parent_class)->dispose(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);
|
|
|
|
g_signal_emit(entry, signals[WORD_CHECK], 0, word, &ret);
|
|
|
|
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_error(entry, start, end);
|
|
}
|
|
|
|
static void
|
|
check_attributes (SexySpellEntry *entry, const char *text, int len)
|
|
{
|
|
gboolean bold = FALSE;
|
|
gboolean italic = FALSE;
|
|
gboolean underline = FALSE;
|
|
int parsing_color = 0;
|
|
char fg_color[3];
|
|
char bg_color[3];
|
|
int i, offset = 0;
|
|
|
|
memset (bg_color, 0, sizeof(bg_color));
|
|
memset (fg_color, 0, sizeof(fg_color));
|
|
|
|
for (i = 0; i < len; i++)
|
|
{
|
|
switch (text[i])
|
|
{
|
|
case ATTR_BOLD:
|
|
insert_hiddenchar (entry, i, i + 1);
|
|
insert_bold (entry, i, bold);
|
|
bold = !bold;
|
|
goto check_color;
|
|
|
|
case ATTR_ITALICS:
|
|
insert_hiddenchar (entry, i, i + 1);
|
|
insert_italic (entry, i, italic);
|
|
italic = !italic;
|
|
goto check_color;
|
|
|
|
case ATTR_UNDERLINE:
|
|
insert_hiddenchar (entry, i, i + 1);
|
|
insert_underline (entry, i, underline);
|
|
underline = !underline;
|
|
goto check_color;
|
|
|
|
case ATTR_RESET:
|
|
insert_hiddenchar (entry, i, i + 1);
|
|
insert_reset (entry, i);
|
|
bold = FALSE;
|
|
italic = FALSE;
|
|
underline = FALSE;
|
|
goto check_color;
|
|
|
|
case ATTR_HIDDEN:
|
|
insert_hiddenchar (entry, i, i + 1);
|
|
goto check_color;
|
|
|
|
case ATTR_REVERSE:
|
|
insert_hiddenchar (entry, i, i + 1);
|
|
insert_color (entry, i, COL_BG, COL_FG);
|
|
goto check_color;
|
|
|
|
case '\n':
|
|
insert_reset (entry, i);
|
|
parsing_color = 0;
|
|
break;
|
|
|
|
case ATTR_COLOR:
|
|
parsing_color = 1;
|
|
offset = 1;
|
|
break;
|
|
|
|
default:
|
|
check_color:
|
|
if (!parsing_color)
|
|
continue;
|
|
|
|
if (!g_unichar_isdigit (text[i]))
|
|
{
|
|
if (text[i] == ',' && parsing_color <= 3)
|
|
{
|
|
parsing_color = 3;
|
|
offset++;
|
|
continue;
|
|
}
|
|
else
|
|
parsing_color = 5;
|
|
}
|
|
|
|
/* don't parse background color without a comma */
|
|
else if (parsing_color == 3 && text[i - 1] != ',')
|
|
parsing_color = 5;
|
|
|
|
switch (parsing_color)
|
|
{
|
|
case 1:
|
|
fg_color[0] = text[i];
|
|
parsing_color++;
|
|
offset++;
|
|
continue;
|
|
case 2:
|
|
fg_color[1] = text[i];
|
|
parsing_color++;
|
|
offset++;
|
|
continue;
|
|
case 3:
|
|
bg_color[0] = text[i];
|
|
parsing_color++;
|
|
offset++;
|
|
continue;
|
|
case 4:
|
|
bg_color[1] = text[i];
|
|
parsing_color++;
|
|
offset++;
|
|
continue;
|
|
case 5:
|
|
if (bg_color[0] != 0)
|
|
{
|
|
insert_hiddenchar (entry, i - offset, i);
|
|
insert_color (entry, i, atoi (fg_color), atoi (bg_color));
|
|
}
|
|
else if (fg_color[0] != 0)
|
|
{
|
|
insert_hiddenchar (entry, i - offset, i);
|
|
insert_color (entry, i, atoi (fg_color), -1);
|
|
}
|
|
else
|
|
{
|
|
/* No colors but some commas may have been added */
|
|
insert_hiddenchar (entry, i - offset, i - offset + 1);
|
|
insert_color (entry, i, -1, -1);
|
|
}
|
|
|
|
memset (bg_color, 0, sizeof(bg_color));
|
|
memset (fg_color, 0, sizeof(fg_color));
|
|
parsing_color = 0;
|
|
offset = 0;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void
|
|
sexy_spell_entry_recheck_all(SexySpellEntry *entry)
|
|
{
|
|
GdkRectangle rect;
|
|
GtkAllocation allocation;
|
|
GtkWidget *widget = GTK_WIDGET(entry);
|
|
PangoLayout *layout;
|
|
int length, i, text_len;
|
|
const char *text;
|
|
|
|
/* 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();
|
|
|
|
if (entry->priv->parseattr)
|
|
{
|
|
/* Check for attributes */
|
|
text = gtk_entry_get_text (GTK_ENTRY (entry));
|
|
text_len = gtk_entry_get_text_length (GTK_ENTRY (entry));
|
|
check_attributes (entry, text, text_len);
|
|
}
|
|
|
|
if (have_enchant && entry->priv->checked
|
|
&& g_slist_length (entry->priv->dict_list) != 0)
|
|
{
|
|
/* 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_get_realized (GTK_WIDGET(entry)))
|
|
{
|
|
gtk_widget_get_allocation (GTK_WIDGET(entry), &allocation);
|
|
|
|
rect.x = 0; rect.y = 0;
|
|
rect.width = allocation.width;
|
|
rect.height = allocation.height;
|
|
gdk_window_invalidate_rect(gtk_widget_get_window (widget), &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;
|
|
|
|
|
|
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;
|
|
const 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));
|
|
log_attrs = pango_layout_get_log_attrs_readonly (layout, &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 || !log_attrs[cend].is_word_boundary)
|
|
&& !log_attrs[cend].is_white)
|
|
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++;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void
|
|
sexy_spell_entry_changed(GtkEditable *editable, gpointer data)
|
|
{
|
|
SexySpellEntry *entry = SEXY_SPELL_ENTRY(editable);
|
|
|
|
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
|
|
* text_spell_langs setting. 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)
|
|
{
|
|
GSList *enchant_langs;
|
|
char *lang, *langs;
|
|
|
|
if (!have_enchant)
|
|
return;
|
|
|
|
if (!entry->priv->broker)
|
|
entry->priv->broker = enchant_broker_init();
|
|
|
|
enchant_langs = sexy_spell_entry_get_languages(entry);
|
|
|
|
langs = g_strdup (prefs.hex_text_spell_langs);
|
|
|
|
lang = strtok (langs, ",");
|
|
while (lang != NULL)
|
|
{
|
|
if (enchant_has_lang (lang, enchant_langs))
|
|
{
|
|
sexy_spell_entry_activate_language_internal (entry, lang, NULL);
|
|
}
|
|
lang = strtok (NULL, ",");
|
|
}
|
|
|
|
g_slist_foreach(enchant_langs, (GFunc) g_free, NULL);
|
|
g_slist_free(enchant_langs);
|
|
g_free (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);
|
|
|
|
sexy_spell_entry_recheck_all (entry);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
enchant_dict_add_to_session (dict, "HexChat", strlen("HexChat"));
|
|
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)
|
|
{
|
|
#ifdef HAVE_ISO_CODES
|
|
const gchar *lang_name = "";
|
|
const gchar *country_name = "";
|
|
|
|
g_return_val_if_fail (have_enchant, NULL);
|
|
|
|
if (codetable_ref == 0)
|
|
codetable_init ();
|
|
|
|
codetable_lookup (lang, &lang_name, &country_name);
|
|
|
|
if (codetable_ref == 0)
|
|
codetable_free ();
|
|
|
|
if (strlen (country_name) != 0)
|
|
return g_strdup_printf ("%s (%s)", lang_name, country_name);
|
|
else
|
|
return g_strdup_printf ("%s", lang_name);
|
|
#else
|
|
return g_strdup (lang);
|
|
#endif
|
|
}
|
|
|
|
/**
|
|
* 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_get_realized (widget))
|
|
{
|
|
/* This will unmark any existing */
|
|
sexy_spell_entry_recheck_all (entry);
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* sexy_spell_entry_set_parse_attributes:
|
|
* @entry: A #SexySpellEntry.
|
|
* @parse: Whether to enable showing attributes
|
|
*
|
|
* Sets whether to enable showing attributes is enabled.
|
|
*/
|
|
void
|
|
sexy_spell_entry_set_parse_attributes (SexySpellEntry *entry, gboolean parse)
|
|
{
|
|
GtkWidget *widget;
|
|
|
|
if (entry->priv->parseattr == parse)
|
|
return;
|
|
|
|
entry->priv->parseattr = parse;
|
|
widget = GTK_WIDGET (entry);
|
|
|
|
if (parse == FALSE && gtk_widget_get_realized (widget))
|
|
{
|
|
/* This will remove current attrs */
|
|
sexy_spell_entry_recheck_all (entry);
|
|
}
|
|
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);
|
|
}
|
|
}
|