865 lines
23 KiB
C
865 lines
23 KiB
C
/*
|
|
* e-spell-entry.c
|
|
*
|
|
* This program 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) version 3.
|
|
*
|
|
* 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
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with the program; if not, see <http://www.gnu.org/licenses/>
|
|
*
|
|
*/
|
|
|
|
/* This code is based on libsexy's SexySpellEntry */
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
#include <config.h>
|
|
#endif
|
|
|
|
#include <glib/gi18n-lib.h>
|
|
#include <gtk/gtk.h>
|
|
|
|
#include <editor/gtkhtml-spell-language.h>
|
|
#include <editor/gtkhtml-spell-checker.h>
|
|
|
|
#include "e-spell-entry.h"
|
|
|
|
enum {
|
|
PROP_0,
|
|
PROP_CHECKING_ENABLED
|
|
};
|
|
|
|
struct _ESpellEntryPrivate
|
|
{
|
|
PangoAttrList *attr_list;
|
|
gint mark_character;
|
|
gint entry_scroll_offset;
|
|
GSettings *settings;
|
|
gboolean custom_checkers;
|
|
gboolean checking_enabled;
|
|
GSList *checkers;
|
|
gchar **words;
|
|
gint *word_starts;
|
|
gint *word_ends;
|
|
};
|
|
|
|
#define E_SPELL_ENTRY_GET_PRIVATE(obj) \
|
|
(G_TYPE_INSTANCE_GET_PRIVATE \
|
|
((obj), E_TYPE_SPELL_ENTRY, ESpellEntryPrivate))
|
|
|
|
G_DEFINE_TYPE (ESpellEntry, e_spell_entry, GTK_TYPE_ENTRY);
|
|
|
|
static gboolean
|
|
word_misspelled (ESpellEntry *entry,
|
|
gint start,
|
|
gint end)
|
|
{
|
|
const gchar *text;
|
|
gchar *word;
|
|
gboolean result = TRUE;
|
|
|
|
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 (g_unichar_isalpha (*word)) {
|
|
GSList *li;
|
|
gssize wlen = strlen (word);
|
|
|
|
for (li = entry->priv->checkers; li; li = g_slist_next (li)) {
|
|
GtkhtmlSpellChecker *checker = li->data;
|
|
if (gtkhtml_spell_checker_check_word (checker, word, wlen)) {
|
|
result = FALSE;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
g_free (word);
|
|
|
|
return result;
|
|
}
|
|
|
|
static void
|
|
insert_underline (ESpellEntry *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
|
|
check_word (ESpellEntry *entry,
|
|
gint start,
|
|
gint 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
|
|
spell_entry_recheck_all (ESpellEntry *entry)
|
|
{
|
|
GtkWidget *widget = GTK_WIDGET (entry);
|
|
PangoLayout *layout;
|
|
gint length, i;
|
|
|
|
if (!entry->priv->words)
|
|
return;
|
|
|
|
/* Remove all existing pango attributes. These will get read as we check */
|
|
pango_attr_list_unref (entry->priv->attr_list);
|
|
entry->priv->attr_list = pango_attr_list_new ();
|
|
|
|
if (entry->priv->checkers && entry->priv->checking_enabled) {
|
|
/* 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 (widget))
|
|
gtk_widget_queue_draw (widget);
|
|
}
|
|
|
|
static void
|
|
get_word_extents_from_position (ESpellEntry *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
|
|
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
|
|
add_to_dictionary (GtkWidget *menuitem,
|
|
ESpellEntry *entry)
|
|
{
|
|
gchar *word;
|
|
gint start, end;
|
|
GtkhtmlSpellChecker *checker;
|
|
|
|
get_word_extents_from_position (entry, &start, &end, entry->priv->mark_character);
|
|
word = gtk_editable_get_chars (GTK_EDITABLE (entry), start, end);
|
|
|
|
checker = g_object_get_data (G_OBJECT (menuitem), "spell-entry-checker");
|
|
if (checker)
|
|
gtkhtml_spell_checker_add_word (checker, 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);
|
|
spell_entry_recheck_all (entry);
|
|
}
|
|
|
|
static void
|
|
ignore_all (GtkWidget *menuitem,
|
|
ESpellEntry *entry)
|
|
{
|
|
gchar *word;
|
|
gint start, end;
|
|
GSList *li;
|
|
|
|
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->checkers; li; li = g_slist_next (li)) {
|
|
GtkhtmlSpellChecker *checker = li->data;
|
|
gtkhtml_spell_checker_add_word_to_session (checker, 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);
|
|
spell_entry_recheck_all (entry);
|
|
}
|
|
|
|
static void
|
|
replace_word (GtkWidget *menuitem,
|
|
ESpellEntry *entry)
|
|
{
|
|
gchar *oldword;
|
|
const gchar *newword;
|
|
gint start, end;
|
|
gint cursor;
|
|
GtkhtmlSpellChecker *checker;
|
|
|
|
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);
|
|
|
|
checker = g_object_get_data (G_OBJECT (menuitem), "spell-entry-checker");
|
|
|
|
if (checker)
|
|
gtkhtml_spell_checker_store_replacement (checker, oldword, -1, newword, -1);
|
|
|
|
g_free (oldword);
|
|
}
|
|
|
|
static void
|
|
build_suggestion_menu (ESpellEntry *entry,
|
|
GtkWidget *menu,
|
|
GtkhtmlSpellChecker *checker,
|
|
const gchar *word)
|
|
{
|
|
GtkWidget *mi;
|
|
GList *suggestions, *iter;
|
|
|
|
suggestions = gtkhtml_spell_checker_get_suggestions (checker, word, -1);
|
|
|
|
if (!suggestions) {
|
|
/* no suggestions. Put something in the menu anyway... */
|
|
GtkWidget *label = gtk_label_new (_("(no suggestions)"));
|
|
PangoAttribute *attribute;
|
|
PangoAttrList *attribute_list;
|
|
|
|
attribute_list = pango_attr_list_new ();
|
|
attribute = pango_attr_style_new (PANGO_STYLE_ITALIC);
|
|
pango_attr_list_insert (attribute_list, attribute);
|
|
gtk_label_set_attributes (GTK_LABEL (label), attribute_list);
|
|
pango_attr_list_unref (attribute_list);
|
|
|
|
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 {
|
|
gint ii = 0;
|
|
|
|
/* build a set of menus with suggestions */
|
|
for (iter = suggestions; iter; iter = g_list_next (iter), ii++) {
|
|
if ((ii != 0) && (ii % 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 (iter->data);
|
|
g_object_set_data (G_OBJECT (mi), "spell-entry-checker", checker);
|
|
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);
|
|
}
|
|
}
|
|
|
|
g_list_free_full (suggestions, g_free);
|
|
}
|
|
|
|
static GtkWidget *
|
|
build_spelling_menu (ESpellEntry *entry,
|
|
const gchar *word)
|
|
{
|
|
GtkhtmlSpellChecker *checker;
|
|
GtkWidget *topmenu, *mi;
|
|
gchar *label;
|
|
|
|
topmenu = gtk_menu_new ();
|
|
|
|
if (!entry->priv->checkers)
|
|
return topmenu;
|
|
|
|
/* Suggestions */
|
|
if (!entry->priv->checkers->next) {
|
|
checker = entry->priv->checkers->data;
|
|
build_suggestion_menu (entry, topmenu, checker, word);
|
|
} else {
|
|
GSList *li;
|
|
GtkWidget *menu;
|
|
const gchar *lang_name;
|
|
|
|
for (li = entry->priv->checkers; li; li = g_slist_next (li)) {
|
|
const GtkhtmlSpellLanguage *language;
|
|
|
|
checker = li->data;
|
|
language = gtkhtml_spell_checker_get_language (checker);
|
|
if (!language)
|
|
continue;
|
|
|
|
lang_name = gtkhtml_spell_language_get_name (language);
|
|
if (!lang_name)
|
|
lang_name = gtkhtml_spell_language_get_code (language);
|
|
|
|
mi = gtk_menu_item_new_with_label (lang_name ? lang_name : "???");
|
|
|
|
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, checker, 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 (!entry->priv->checkers->next) {
|
|
checker = entry->priv->checkers->data;
|
|
g_object_set_data (G_OBJECT (mi), "spell-entry-checker", checker);
|
|
g_signal_connect (G_OBJECT (mi), "activate", G_CALLBACK (add_to_dictionary), entry);
|
|
} else {
|
|
GSList *li;
|
|
GtkWidget *menu, *submi;
|
|
const gchar *lang_name;
|
|
|
|
menu = gtk_menu_new ();
|
|
gtk_menu_item_set_submenu (GTK_MENU_ITEM (mi), menu);
|
|
|
|
for (li = entry->priv->checkers; li; li = g_slist_next (li)) {
|
|
const GtkhtmlSpellLanguage *language;
|
|
|
|
checker = li->data;
|
|
language = gtkhtml_spell_checker_get_language (checker);
|
|
if (!language)
|
|
continue;
|
|
|
|
lang_name = gtkhtml_spell_language_get_name (language);
|
|
if (!lang_name)
|
|
lang_name = gtkhtml_spell_language_get_code (language);
|
|
|
|
submi = gtk_menu_item_new_with_label (lang_name ? lang_name : "???");
|
|
g_object_set_data (G_OBJECT (submi), "spell-entry-checker", checker);
|
|
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
|
|
spell_entry_add_suggestions_menu (ESpellEntry *entry,
|
|
GtkMenu *menu,
|
|
const gchar *word)
|
|
{
|
|
GtkWidget *icon, *mi;
|
|
|
|
g_return_if_fail (menu != NULL);
|
|
g_return_if_fail (word != NULL);
|
|
|
|
/* 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);
|
|
|
|
gtk_menu_item_set_submenu (GTK_MENU_ITEM (mi), build_spelling_menu (entry, word));
|
|
|
|
gtk_widget_show_all (mi);
|
|
gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), mi);
|
|
}
|
|
|
|
static gboolean
|
|
spell_entry_popup_menu (ESpellEntry *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
|
|
spell_entry_populate_popup (ESpellEntry *entry,
|
|
GtkMenu *menu,
|
|
gpointer data)
|
|
{
|
|
gint start, end;
|
|
gchar *word;
|
|
|
|
if (!entry->priv->checkers)
|
|
return;
|
|
|
|
get_word_extents_from_position (entry, &start, &end, entry->priv->mark_character);
|
|
if (start == end)
|
|
return;
|
|
|
|
if (!word_misspelled (entry, start, end))
|
|
return;
|
|
|
|
word = gtk_editable_get_chars (GTK_EDITABLE (entry), start, end);
|
|
g_return_if_fail (word != NULL);
|
|
|
|
spell_entry_add_suggestions_menu (entry, menu, word);
|
|
|
|
g_free (word);
|
|
}
|
|
|
|
static void
|
|
spell_entry_changed (GtkEditable *editable)
|
|
{
|
|
ESpellEntry *entry = E_SPELL_ENTRY (editable);
|
|
|
|
if (!entry->priv->checkers)
|
|
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);
|
|
spell_entry_recheck_all (entry);
|
|
}
|
|
|
|
static void
|
|
spell_entry_notify_scroll_offset (ESpellEntry *spell_entry)
|
|
{
|
|
g_return_if_fail (spell_entry != NULL);
|
|
|
|
g_object_get (G_OBJECT (spell_entry), "scroll-offset", &spell_entry->priv->entry_scroll_offset, NULL);
|
|
}
|
|
|
|
static GList *
|
|
spell_entry_load_spell_languages (void)
|
|
{
|
|
GSettings *settings;
|
|
GList *spell_languages = NULL;
|
|
gchar **strv;
|
|
gint ii;
|
|
|
|
/* Ask GSettings for a list of spell check language codes. */
|
|
settings = g_settings_new ("org.gnome.evolution.mail");
|
|
strv = g_settings_get_strv (settings, "composer-spell-languages");
|
|
g_object_unref (settings);
|
|
|
|
/* Convert the codes to spell language structs. */
|
|
for (ii = 0; strv[ii] != NULL; ii++) {
|
|
gchar *language_code = strv[ii];
|
|
const GtkhtmlSpellLanguage *language;
|
|
|
|
language = gtkhtml_spell_language_lookup (language_code);
|
|
if (language != NULL)
|
|
spell_languages = g_list_prepend (
|
|
spell_languages, (gpointer) language);
|
|
}
|
|
|
|
g_strfreev (strv);
|
|
|
|
spell_languages = g_list_reverse (spell_languages);
|
|
|
|
/* Pick a default spell language if it came back empty. */
|
|
if (spell_languages == NULL) {
|
|
const GtkhtmlSpellLanguage *language;
|
|
|
|
language = gtkhtml_spell_language_lookup (NULL);
|
|
|
|
if (language) {
|
|
spell_languages = g_list_prepend (
|
|
spell_languages, (gpointer) language);
|
|
}
|
|
}
|
|
|
|
return spell_languages;
|
|
}
|
|
|
|
static void
|
|
spell_entry_settings_changed (ESpellEntry *spell_entry,
|
|
const gchar *key)
|
|
{
|
|
GList *languages;
|
|
|
|
g_return_if_fail (spell_entry != NULL);
|
|
|
|
if (spell_entry->priv->custom_checkers)
|
|
return;
|
|
|
|
if (key && !g_str_equal (key, "composer-spell-languages"))
|
|
return;
|
|
|
|
languages = spell_entry_load_spell_languages ();
|
|
e_spell_entry_set_languages (spell_entry, languages);
|
|
g_list_free (languages);
|
|
|
|
spell_entry->priv->custom_checkers = FALSE;
|
|
}
|
|
|
|
static gint
|
|
spell_entry_find_position (ESpellEntry *spell_entry,
|
|
gint x)
|
|
{
|
|
PangoLayout *layout;
|
|
PangoLayoutLine *line;
|
|
gint index;
|
|
gint pos;
|
|
gint trailing;
|
|
const gchar *text;
|
|
GtkEntry *entry = GTK_ENTRY (spell_entry);
|
|
|
|
layout = gtk_entry_get_layout (entry);
|
|
text = pango_layout_get_text (layout);
|
|
|
|
line = pango_layout_get_lines_readonly (layout)->data;
|
|
pango_layout_line_x_to_index (line, x * PANGO_SCALE, &index, &trailing);
|
|
|
|
pos = g_utf8_pointer_to_offset (text, text + index);
|
|
pos += trailing;
|
|
|
|
return pos;
|
|
}
|
|
|
|
static gboolean
|
|
e_spell_entry_draw (GtkWidget *widget,
|
|
cairo_t *cr)
|
|
{
|
|
ESpellEntry *spell_entry = E_SPELL_ENTRY (widget);
|
|
GtkEntry *entry = GTK_ENTRY (widget);
|
|
PangoLayout *layout;
|
|
|
|
layout = gtk_entry_get_layout (entry);
|
|
pango_layout_set_attributes (layout, spell_entry->priv->attr_list);
|
|
|
|
return GTK_WIDGET_CLASS (e_spell_entry_parent_class)->draw (widget, cr);
|
|
}
|
|
|
|
static gboolean
|
|
e_spell_entry_button_press (GtkWidget *widget,
|
|
GdkEventButton *event)
|
|
{
|
|
ESpellEntry *spell_entry = E_SPELL_ENTRY (widget);
|
|
|
|
spell_entry->priv->mark_character = spell_entry_find_position (
|
|
spell_entry, event->x + spell_entry->priv->entry_scroll_offset);
|
|
|
|
return GTK_WIDGET_CLASS (e_spell_entry_parent_class)->button_press_event (widget, event);
|
|
}
|
|
|
|
static void
|
|
spell_entry_set_property (GObject *object,
|
|
guint property_id,
|
|
const GValue *value,
|
|
GParamSpec *pspec)
|
|
{
|
|
switch (property_id) {
|
|
case PROP_CHECKING_ENABLED:
|
|
e_spell_entry_set_checking_enabled (
|
|
E_SPELL_ENTRY (object),
|
|
g_value_get_boolean (value));
|
|
return;
|
|
}
|
|
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
|
}
|
|
|
|
static void
|
|
spell_entry_get_property (GObject *object,
|
|
guint property_id,
|
|
GValue *value,
|
|
GParamSpec *pspec)
|
|
{
|
|
switch (property_id) {
|
|
case PROP_CHECKING_ENABLED:
|
|
g_value_set_boolean (
|
|
value,
|
|
e_spell_entry_get_checking_enabled (
|
|
E_SPELL_ENTRY (object)));
|
|
return;
|
|
}
|
|
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
|
}
|
|
|
|
static void
|
|
e_spell_entry_init (ESpellEntry *spell_entry)
|
|
{
|
|
spell_entry->priv = E_SPELL_ENTRY_GET_PRIVATE (spell_entry);
|
|
spell_entry->priv->attr_list = pango_attr_list_new ();
|
|
spell_entry->priv->checkers = NULL;
|
|
spell_entry->priv->checking_enabled = TRUE;
|
|
|
|
g_signal_connect (spell_entry, "popup-menu", G_CALLBACK (spell_entry_popup_menu), NULL);
|
|
g_signal_connect (spell_entry, "populate-popup", G_CALLBACK (spell_entry_populate_popup), NULL);
|
|
g_signal_connect (spell_entry, "changed", G_CALLBACK (spell_entry_changed), NULL);
|
|
g_signal_connect (spell_entry, "notify::scroll-offset", G_CALLBACK (spell_entry_notify_scroll_offset), NULL);
|
|
|
|
/* listen for languages changes */
|
|
spell_entry->priv->settings = g_settings_new ("org.gnome.evolution.mail");
|
|
g_signal_connect_swapped (spell_entry->priv->settings, "changed", G_CALLBACK (spell_entry_settings_changed), spell_entry);
|
|
|
|
/* load current settings */
|
|
spell_entry_settings_changed (spell_entry, NULL);
|
|
}
|
|
|
|
static void
|
|
e_spell_entry_finalize (GObject *object)
|
|
{
|
|
ESpellEntry *entry;
|
|
|
|
g_return_if_fail (object != NULL);
|
|
g_return_if_fail (E_IS_SPELL_ENTRY (object));
|
|
|
|
entry = E_SPELL_ENTRY (object);
|
|
|
|
if (entry->priv->settings)
|
|
g_object_unref (entry->priv->settings);
|
|
if (entry->priv->checkers)
|
|
g_slist_free_full (entry->priv->checkers, g_object_unref);
|
|
if (entry->priv->attr_list)
|
|
pango_attr_list_unref (entry->priv->attr_list);
|
|
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);
|
|
|
|
G_OBJECT_CLASS (e_spell_entry_parent_class)->finalize (object);
|
|
}
|
|
|
|
static void
|
|
e_spell_entry_class_init (ESpellEntryClass *class)
|
|
{
|
|
GObjectClass *object_class;
|
|
GtkWidgetClass *widget_class;
|
|
|
|
g_type_class_add_private (class, sizeof (ESpellEntryPrivate));
|
|
|
|
object_class = G_OBJECT_CLASS (class);
|
|
object_class->set_property = spell_entry_set_property;
|
|
object_class->get_property = spell_entry_get_property;
|
|
object_class->finalize = e_spell_entry_finalize;
|
|
|
|
widget_class = GTK_WIDGET_CLASS (class);
|
|
widget_class->draw = e_spell_entry_draw;
|
|
widget_class->button_press_event = e_spell_entry_button_press;
|
|
|
|
g_object_class_install_property (
|
|
object_class,
|
|
PROP_CHECKING_ENABLED,
|
|
g_param_spec_boolean (
|
|
"checking-enabled",
|
|
"checking enabled",
|
|
"Spell Checking is Enabled",
|
|
TRUE,
|
|
G_PARAM_READWRITE));
|
|
}
|
|
|
|
GtkWidget *
|
|
e_spell_entry_new (void)
|
|
{
|
|
return g_object_new (E_TYPE_SPELL_ENTRY, NULL);
|
|
}
|
|
|
|
/* 'languages' consists of 'const GtkhtmlSpellLanguage *' */
|
|
void
|
|
e_spell_entry_set_languages (ESpellEntry *spell_entry,
|
|
GList *languages)
|
|
{
|
|
GList *iter;
|
|
|
|
g_return_if_fail (spell_entry != NULL);
|
|
|
|
spell_entry->priv->custom_checkers = TRUE;
|
|
|
|
if (spell_entry->priv->checkers)
|
|
g_slist_free_full (spell_entry->priv->checkers, g_object_unref);
|
|
spell_entry->priv->checkers = NULL;
|
|
|
|
for (iter = languages; iter; iter = g_list_next (iter)) {
|
|
const GtkhtmlSpellLanguage *language = iter->data;
|
|
|
|
if (language)
|
|
spell_entry->priv->checkers = g_slist_prepend (spell_entry->priv->checkers,
|
|
gtkhtml_spell_checker_new (language));
|
|
}
|
|
|
|
spell_entry->priv->checkers = g_slist_reverse (spell_entry->priv->checkers);
|
|
|
|
if (gtk_widget_get_realized (GTK_WIDGET (spell_entry)))
|
|
spell_entry_recheck_all (spell_entry);
|
|
}
|
|
|
|
gboolean
|
|
e_spell_entry_get_checking_enabled (ESpellEntry *spell_entry)
|
|
{
|
|
g_return_val_if_fail (spell_entry != NULL, FALSE);
|
|
|
|
return spell_entry->priv->checking_enabled;
|
|
}
|
|
|
|
void
|
|
e_spell_entry_set_checking_enabled (ESpellEntry *spell_entry,
|
|
gboolean enable_checking)
|
|
{
|
|
g_return_if_fail (spell_entry != NULL);
|
|
|
|
if ((enable_checking ? 1 : 0) == (spell_entry->priv->checking_enabled ? 1 : 0))
|
|
return;
|
|
|
|
spell_entry->priv->checking_enabled = enable_checking;
|
|
spell_entry_recheck_all (spell_entry);
|
|
|
|
g_object_notify (G_OBJECT (spell_entry), "checking-enabled");
|
|
|
|
}
|