[gnome-text-editor/wip/chergert/gspell: 10/10] wip: stub out cribbing of gspell
- From: Christian Hergert <chergert src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-text-editor/wip/chergert/gspell: 10/10] wip: stub out cribbing of gspell
- Date: Thu, 24 Jun 2021 17:24:11 +0000 (UTC)
commit 5a026c590432827c904fd83d94a562f6a42e9c2c
Author: Christian Hergert <chergert redhat com>
Date: Mon Jun 21 17:51:03 2021 -0700
wip: stub out cribbing of gspell
If we want to have this on GTK 4 until GTK 4 itself has spell check
support, we'll need to handle that internally.
gspell/checker-dialog.ui | 255 +++++
gspell/gconstructor.h | 94 ++
gspell/gspell-checker-dialog.c | 744 ++++++++++++++
gspell/gspell-checker-dialog.h | 60 ++
gspell/gspell-checker-private.h | 35 +
gspell/gspell-checker.c | 667 +++++++++++++
gspell/gspell-checker.h | 134 +++
gspell/gspell-context-menu.c | 347 +++++++
gspell/gspell-context-menu.h | 50 +
gspell/gspell-current-word-policy.c | 248 +++++
gspell/gspell-current-word-policy.h | 87 ++
gspell/gspell-entry-buffer.c | 253 +++++
gspell/gspell-entry-buffer.h | 57 ++
gspell/gspell-entry-private.h | 34 +
gspell/gspell-entry-utils.c | 252 +++++
gspell/gspell-entry-utils.h | 62 ++
gspell/gspell-entry.c | 1234 +++++++++++++++++++++++
gspell/gspell-entry.h | 59 ++
gspell/gspell-enum-types.c.template | 45 +
gspell/gspell-enum-types.h.template | 33 +
gspell/gspell-icu.c | 249 +++++
gspell/gspell-icu.h | 39 +
gspell/gspell-init.c | 170 ++++
gspell/gspell-inline-checker-text-buffer.c | 1485 ++++++++++++++++++++++++++++
gspell/gspell-inline-checker-text-buffer.h | 69 ++
gspell/gspell-language-chooser-button.c | 328 ++++++
gspell/gspell-language-chooser-button.h | 55 ++
gspell/gspell-language-chooser-dialog.c | 498 ++++++++++
gspell/gspell-language-chooser-dialog.h | 58 ++
gspell/gspell-language-chooser.c | 177 ++++
gspell/gspell-language-chooser.h | 72 ++
gspell/gspell-language.c | 312 ++++++
gspell/gspell-language.h | 77 ++
gspell/gspell-navigator-text-view.c | 553 +++++++++++
gspell/gspell-navigator-text-view.h | 59 ++
gspell/gspell-navigator.c | 171 ++++
gspell/gspell-navigator.h | 78 ++
gspell/gspell-osx.c | 68 ++
gspell/gspell-osx.h | 38 +
gspell/gspell-text-buffer.c | 275 ++++++
gspell/gspell-text-buffer.h | 57 ++
gspell/gspell-text-iter.c | 193 ++++
gspell/gspell-text-iter.h | 46 +
gspell/gspell-text-view.c | 615 ++++++++++++
gspell/gspell-text-view.h | 74 ++
gspell/gspell-utils.c | 284 ++++++
gspell/gspell-utils.h | 72 ++
gspell/gspell-version.h | 44 +
gspell/gspell.gresource.xml | 7 +
gspell/gspell.h | 45 +
gspell/gspellregion.c | 1371 +++++++++++++++++++++++++
gspell/gspellregion.h | 125 +++
gspell/language-dialog.ui | 77 ++
gspell/meson.build | 51 +
meson.build | 4 +
55 files changed, 12646 insertions(+)
---
diff --git a/gspell/checker-dialog.ui b/gspell/checker-dialog.ui
new file mode 100644
index 0000000..70911b9
--- /dev/null
+++ b/gspell/checker-dialog.ui
@@ -0,0 +1,255 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.19.0 -->
+<interface domain="gspell-1">
+ <requires lib="gtk+" version="3.16"/>
+ <template class="GspellCheckerDialog" parent="GtkDialog">
+ <property name="can_focus">False</property>
+ <property name="title" translatable="yes">Check Spelling</property>
+ <property name="resizable">False</property>
+ <property name="type_hint">dialog</property>
+
+ <!-- Modal because it is not supported to edit the buffer during a spell
+ checking with the CheckerDialog. What if we modify directly in the
+ buffer the word being spell checked, and then we click on the Change
+ button? there is now a critical message if the word is no longer the
+ same. See the thoughts at:
+ https://bugzilla.gnome.org/show_bug.cgi?id=761923 -->
+ <property name="modal">True</property>
+
+ <child internal-child="vbox">
+ <object class="GtkBox" id="content">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">6</property>
+ <property name="margin">12</property>
+ <child>
+ <object class="GtkGrid" id="grid1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="row_spacing">6</property>
+ <property name="column_spacing">6</property>
+ <child>
+ <object class="GtkLabel" id="label1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Misspelled word:</property>
+ <property name="justify">center</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="misspelled_word_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">word</property>
+ <property name="use_markup">True</property>
+ <property name="wrap">True</property>
+ <property name="max_width_chars">72</property>
+ <property name="selectable">True</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ <property name="width">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label3">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Change _to:</property>
+ <property name="use_underline">True</property>
+ <property name="justify">center</property>
+ <property name="mnemonic_widget">word_entry</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="word_entry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hexpand">True</property>
+ <property name="activates_default">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="check_word_button">
+ <property name="label" translatable="yes">Check _Word</property>
+ <property name="visible">True</property>
+ <property name="sensitive">False</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_underline">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkGrid" id="grid2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="row_spacing">6</property>
+ <property name="column_spacing">6</property>
+ <child>
+ <object class="GtkLabel" id="label4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">_Suggestions:</property>
+ <property name="use_underline">True</property>
+ <property name="justify">center</property>
+ <property name="mnemonic_widget">suggestions_view</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="ignore_button">
+ <property name="label" translatable="yes">_Ignore</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_underline">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="ignore_all_button">
+ <property name="label" translatable="yes">Ignore _All</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_underline">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="change_button">
+ <property name="label" translatable="yes">Cha_nge</property>
+ <property name="visible">True</property>
+ <property name="sensitive">False</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_underline">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="change_all_button">
+ <property name="label" translatable="yes">Change A_ll</property>
+ <property name="visible">True</property>
+ <property name="sensitive">False</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_underline">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="top_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label6">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_top">6</property>
+ <property name="label" translatable="yes">User dictionary:</property>
+ <property name="use_markup">True</property>
+ <property name="xalign">7.4505801528346183e-09</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">3</property>
+ <property name="width">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="add_word_button">
+ <property name="label" translatable="yes">Add w_ord</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="use_underline">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">4</property>
+ <property name="width">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolledwindow1">
+ <property name="width_request">200</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="shadow_type">etched-in</property>
+ <child>
+ <object class="GtkTreeView" id="suggestions_view">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="headers_visible">False</property>
+ <property name="expand">True</property>
+ <child internal-child="selection">
+ <object class="GtkTreeSelection" id="treeview-selection"/>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ <property name="height">4</property>
+ </packing>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/gspell/gconstructor.h b/gspell/gconstructor.h
new file mode 100644
index 0000000..df98f83
--- /dev/null
+++ b/gspell/gconstructor.h
@@ -0,0 +1,94 @@
+/*
+ If G_HAS_CONSTRUCTORS is true then the compiler support *both* constructors and
+ destructors, in a sane way, including e.g. on library unload. If not you're on
+ your own.
+
+ Some compilers need #pragma to handle this, which does not work with macros,
+ so the way you need to use this is (for constructors):
+
+ #ifdef G_DEFINE_CONSTRUCTOR_NEEDS_PRAGMA
+ #pragma G_DEFINE_CONSTRUCTOR_PRAGMA_ARGS(my_constructor)
+ #endif
+ G_DEFINE_CONSTRUCTOR(my_constructor)
+ static void my_constructor(void) {
+ ...
+ }
+
+*/
+
+#ifndef __GTK_DOC_IGNORE__
+
+#if __GNUC__ > 2 || (__GNUC__ == 2 && __GNUC_MINOR__ >= 7)
+
+#define G_HAS_CONSTRUCTORS 1
+
+#define G_DEFINE_CONSTRUCTOR(_func) static void __attribute__((constructor)) _func (void);
+#define G_DEFINE_DESTRUCTOR(_func) static void __attribute__((destructor)) _func (void);
+
+#elif defined (_MSC_VER) && (_MSC_VER >= 1500)
+/* Visual studio 2008 and later has _Pragma */
+
+#define G_HAS_CONSTRUCTORS 1
+
+#define G_DEFINE_CONSTRUCTOR(_func) \
+ static void _func(void); \
+ static int _func ## _wrapper(void) { _func(); return 0; } \
+ __pragma(section(".CRT$XCU",read)) \
+ __declspec(allocate(".CRT$XCU")) static int (* _array ## _func)(void) = _func ## _wrapper;
+
+#define G_DEFINE_DESTRUCTOR(_func) \
+ static void _func(void); \
+ static int _func ## _constructor(void) { atexit (_func); return 0; } \
+ __pragma(section(".CRT$XCU",read)) \
+ __declspec(allocate(".CRT$XCU")) static int (* _array ## _func)(void) = _func ## _constructor;
+
+#elif defined (_MSC_VER)
+
+#define G_HAS_CONSTRUCTORS 1
+
+/* Pre Visual studio 2008 must use #pragma section */
+#define G_DEFINE_CONSTRUCTOR_NEEDS_PRAGMA 1
+#define G_DEFINE_DESTRUCTOR_NEEDS_PRAGMA 1
+
+#define G_DEFINE_CONSTRUCTOR_PRAGMA_ARGS(_func) \
+ section(".CRT$XCU",read)
+#define G_DEFINE_CONSTRUCTOR(_func) \
+ static void _func(void); \
+ static int _func ## _wrapper(void) { _func(); return 0; } \
+ __declspec(allocate(".CRT$XCU")) static int (*p)(void) = _func ## _wrapper;
+
+#define G_DEFINE_DESTRUCTOR_PRAGMA_ARGS(_func) \
+ section(".CRT$XCU",read)
+#define G_DEFINE_DESTRUCTOR(_func) \
+ static void _func(void); \
+ static int _func ## _constructor(void) { atexit (_func); return 0; } \
+ __declspec(allocate(".CRT$XCU")) static int (* _array ## _func)(void) = _func ## _constructor;
+
+#elif defined(__SUNPRO_C)
+
+/* This is not tested, but i believe it should work, based on:
+ * http://opensource.apple.com/source/OpenSSL098/OpenSSL098-35/src/fips/fips_premain.c
+ */
+
+#define G_HAS_CONSTRUCTORS 1
+
+#define G_DEFINE_CONSTRUCTOR_NEEDS_PRAGMA 1
+#define G_DEFINE_DESTRUCTOR_NEEDS_PRAGMA 1
+
+#define G_DEFINE_CONSTRUCTOR_PRAGMA_ARGS(_func) \
+ init(_func)
+#define G_DEFINE_CONSTRUCTOR(_func) \
+ static void _func(void);
+
+#define G_DEFINE_DESTRUCTOR_PRAGMA_ARGS(_func) \
+ fini(_func)
+#define G_DEFINE_DESTRUCTOR(_func) \
+ static void _func(void);
+
+#else
+
+/* constructors not supported for this compiler */
+
+#endif
+
+#endif /* __GTK_DOC_IGNORE__ */
diff --git a/gspell/gspell-checker-dialog.c b/gspell/gspell-checker-dialog.c
new file mode 100644
index 0000000..0a699af
--- /dev/null
+++ b/gspell/gspell-checker-dialog.c
@@ -0,0 +1,744 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2002 - Paolo Maggi
+ * Copyright 2015 - Sébastien Wilmet
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "gspell-checker-dialog.h"
+#include <glib/gi18n-lib.h>
+#include "gspell-checker.h"
+
+/**
+ * SECTION:checker-dialog
+ * @Short_description: Spell checker dialog
+ * @Title: GspellCheckerDialog
+ * @See_also: #GspellNavigator
+ *
+ * #GspellCheckerDialog is a #GtkDialog to spell check a document one word
+ * at a time. It uses a #GspellNavigator.
+ */
+
+typedef struct _GspellCheckerDialogPrivate GspellCheckerDialogPrivate;
+
+struct _GspellCheckerDialogPrivate
+{
+ GspellNavigator *navigator;
+ GspellChecker *checker;
+
+ gchar *misspelled_word;
+
+ GtkLabel *misspelled_word_label;
+ GtkEntry *word_entry;
+ GtkWidget *check_word_button;
+ GtkWidget *ignore_button;
+ GtkWidget *ignore_all_button;
+ GtkWidget *change_button;
+ GtkWidget *change_all_button;
+ GtkWidget *add_word_button;
+ GtkTreeView *suggestions_view;
+
+ guint initialized : 1;
+};
+
+enum
+{
+ PROP_0,
+ PROP_SPELL_NAVIGATOR,
+};
+
+enum
+{
+ COLUMN_SUGGESTION,
+ N_COLUMNS
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (GspellCheckerDialog, gspell_checker_dialog, GTK_TYPE_DIALOG)
+
+static void
+set_spell_checker (GspellCheckerDialog *dialog,
+ GspellChecker *checker)
+{
+ GspellCheckerDialogPrivate *priv;
+
+ priv = gspell_checker_dialog_get_instance_private (dialog);
+
+ if (g_set_object (&priv->checker, checker))
+ {
+ GtkHeaderBar *header_bar;
+ const GspellLanguage *lang;
+
+ header_bar = GTK_HEADER_BAR (gtk_dialog_get_header_bar (GTK_DIALOG (dialog)));
+
+ lang = gspell_checker_get_language (checker);
+
+ gtk_header_bar_set_subtitle (header_bar,
+ gspell_language_get_name (lang));
+ }
+}
+
+static void
+set_navigator (GspellCheckerDialog *dialog,
+ GspellNavigator *navigator)
+{
+ GspellCheckerDialogPrivate *priv;
+
+ priv = gspell_checker_dialog_get_instance_private (dialog);
+
+ g_return_if_fail (priv->navigator == NULL);
+ priv->navigator = g_object_ref_sink (navigator);
+
+ g_object_notify (G_OBJECT (dialog), "spell-navigator");
+}
+
+static void
+clear_suggestions (GspellCheckerDialog *dialog)
+{
+ GspellCheckerDialogPrivate *priv;
+ GtkListStore *store;
+
+ priv = gspell_checker_dialog_get_instance_private (dialog);
+
+ store = GTK_LIST_STORE (gtk_tree_view_get_model (priv->suggestions_view));
+ gtk_list_store_clear (store);
+
+ gtk_tree_view_columns_autosize (priv->suggestions_view);
+}
+
+static void
+set_suggestions (GspellCheckerDialog *dialog,
+ GSList *suggestions)
+{
+ GspellCheckerDialogPrivate *priv;
+ GtkListStore *store;
+ GtkTreeIter iter;
+ GtkTreeSelection *selection;
+ const gchar *first_suggestion;
+ GSList *l;
+
+ priv = gspell_checker_dialog_get_instance_private (dialog);
+
+ clear_suggestions (dialog);
+
+ store = GTK_LIST_STORE (gtk_tree_view_get_model (priv->suggestions_view));
+
+ if (suggestions == NULL)
+ {
+ gtk_list_store_append (store, &iter);
+ gtk_list_store_set (store, &iter,
+ /* Translators: Displayed in the "Check Spelling"
+ * dialog if there are no suggestions for the current
+ * misspelled word.
+ */
+ COLUMN_SUGGESTION, _("(no suggested words)"),
+ -1);
+
+ gtk_entry_set_text (priv->word_entry, "");
+ gtk_widget_set_sensitive (GTK_WIDGET (priv->suggestions_view), FALSE);
+ return;
+ }
+
+ gtk_widget_set_sensitive (GTK_WIDGET (priv->suggestions_view), TRUE);
+
+ first_suggestion = suggestions->data;
+ gtk_entry_set_text (priv->word_entry, first_suggestion);
+
+ for (l = suggestions; l != NULL; l = l->next)
+ {
+ const gchar *suggestion = l->data;
+
+ gtk_list_store_append (store, &iter);
+ gtk_list_store_set (store, &iter,
+ COLUMN_SUGGESTION, suggestion,
+ -1);
+ }
+
+ selection = gtk_tree_view_get_selection (priv->suggestions_view);
+ gtk_tree_model_get_iter_first (GTK_TREE_MODEL (store), &iter);
+ gtk_tree_selection_select_iter (selection, &iter);
+}
+
+static void
+set_misspelled_word (GspellCheckerDialog *dialog,
+ const gchar *word)
+{
+ GspellCheckerDialogPrivate *priv;
+ gchar *label;
+ GSList *suggestions;
+
+ g_assert (word != NULL);
+
+ priv = gspell_checker_dialog_get_instance_private (dialog);
+
+ g_return_if_fail (!gspell_checker_check_word (priv->checker, word, -1, NULL));
+
+ g_free (priv->misspelled_word);
+ priv->misspelled_word = g_strdup (word);
+
+ label = g_strdup_printf("<b>%s</b>", word);
+ gtk_label_set_markup (priv->misspelled_word_label, label);
+ g_free (label);
+
+ suggestions = gspell_checker_get_suggestions (priv->checker, priv->misspelled_word, -1);
+
+ set_suggestions (dialog, suggestions);
+
+ g_slist_free_full (suggestions, g_free);
+}
+
+static void
+set_completed (GspellCheckerDialog *dialog)
+{
+ GspellCheckerDialogPrivate *priv;
+
+ priv = gspell_checker_dialog_get_instance_private (dialog);
+
+ clear_suggestions (dialog);
+ gtk_entry_set_text (priv->word_entry, "");
+
+ gtk_widget_set_sensitive (GTK_WIDGET (priv->word_entry), FALSE);
+ gtk_widget_set_sensitive (priv->check_word_button, FALSE);
+ gtk_widget_set_sensitive (priv->ignore_button, FALSE);
+ gtk_widget_set_sensitive (priv->ignore_all_button, FALSE);
+ gtk_widget_set_sensitive (priv->change_button, FALSE);
+ gtk_widget_set_sensitive (priv->change_all_button, FALSE);
+ gtk_widget_set_sensitive (priv->add_word_button, FALSE);
+ gtk_widget_set_sensitive (GTK_WIDGET (priv->suggestions_view), FALSE);
+}
+
+static void
+show_error (GspellCheckerDialog *dialog,
+ GError *error)
+{
+ GspellCheckerDialogPrivate *priv;
+ gchar *label;
+
+ priv = gspell_checker_dialog_get_instance_private (dialog);
+
+ label = g_strdup_printf ("<b>%s</b> %s", _("Error:"), error->message);
+ gtk_label_set_markup (priv->misspelled_word_label, label);
+ g_free (label);
+
+ set_completed (dialog);
+}
+
+static void
+goto_next (GspellCheckerDialog *dialog)
+{
+ GspellCheckerDialogPrivate *priv;
+ gchar *word = NULL;
+ GspellChecker *checker = NULL;
+ GError *error = NULL;
+ gboolean found;
+
+ priv = gspell_checker_dialog_get_instance_private (dialog);
+
+ found = gspell_navigator_goto_next (priv->navigator, &word, &checker, &error);
+
+ if (error != NULL)
+ {
+ show_error (dialog, error);
+ g_clear_error (&error);
+ }
+ else if (found)
+ {
+ set_spell_checker (dialog, checker);
+ set_misspelled_word (dialog, word);
+ }
+ else
+ {
+ gchar *label;
+
+ if (priv->initialized)
+ {
+ label = g_strdup_printf ("<b>%s</b>", _("Completed spell checking"));
+ }
+ else
+ {
+ label = g_strdup_printf ("<b>%s</b>", _("No misspelled words"));
+ }
+
+ gtk_label_set_markup (priv->misspelled_word_label, label);
+ g_free (label);
+
+ set_completed (dialog);
+ }
+
+ priv->initialized = TRUE;
+
+ g_free (word);
+ g_clear_object (&checker);
+}
+
+static void
+gspell_checker_dialog_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GspellCheckerDialog *dialog = GSPELL_CHECKER_DIALOG (object);
+
+ switch (prop_id)
+ {
+ case PROP_SPELL_NAVIGATOR:
+ g_value_set_object (value, gspell_checker_dialog_get_spell_navigator (dialog));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gspell_checker_dialog_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GspellCheckerDialog *dialog = GSPELL_CHECKER_DIALOG (object);
+
+ switch (prop_id)
+ {
+ case PROP_SPELL_NAVIGATOR:
+ set_navigator (dialog, g_value_get_object (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gspell_checker_dialog_dispose (GObject *object)
+{
+ GspellCheckerDialogPrivate *priv;
+
+ priv = gspell_checker_dialog_get_instance_private (GSPELL_CHECKER_DIALOG (object));
+
+ g_clear_object (&priv->navigator);
+ g_clear_object (&priv->checker);
+
+ G_OBJECT_CLASS (gspell_checker_dialog_parent_class)->dispose (object);
+}
+
+static void
+gspell_checker_dialog_finalize (GObject *object)
+{
+ GspellCheckerDialogPrivate *priv;
+
+ priv = gspell_checker_dialog_get_instance_private (GSPELL_CHECKER_DIALOG (object));
+
+ g_free (priv->misspelled_word);
+
+ G_OBJECT_CLASS (gspell_checker_dialog_parent_class)->finalize (object);
+}
+
+static void
+gspell_checker_dialog_show (GtkWidget *widget)
+{
+ GspellCheckerDialog *dialog = GSPELL_CHECKER_DIALOG (widget);
+ GspellCheckerDialogPrivate *priv;
+
+ priv = gspell_checker_dialog_get_instance_private (dialog);
+
+ /* Chain-up */
+ if (GTK_WIDGET_CLASS (gspell_checker_dialog_parent_class)->show != NULL)
+ {
+ GTK_WIDGET_CLASS (gspell_checker_dialog_parent_class)->show (widget);
+ }
+
+ /* A typical implementation of a SpellNavigator is to select the
+ * misspelled word when goto_next() is called. Showing the dialog makes
+ * a focus change, which can unselect the buffer selection (e.g. in a
+ * GtkTextBuffer). So that's why goto_next() is called after the
+ * chain-up.
+ */
+ if (!priv->initialized)
+ {
+ goto_next (dialog);
+ }
+}
+
+static void
+gspell_checker_dialog_class_init (GspellCheckerDialogClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->get_property = gspell_checker_dialog_get_property;
+ object_class->set_property = gspell_checker_dialog_set_property;
+ object_class->dispose = gspell_checker_dialog_dispose;
+ object_class->finalize = gspell_checker_dialog_finalize;
+
+ widget_class->show = gspell_checker_dialog_show;
+
+ /**
+ * GspellCheckerDialog:spell-navigator:
+ *
+ * The #GspellNavigator to use.
+ */
+ g_object_class_install_property (object_class,
+ PROP_SPELL_NAVIGATOR,
+ g_param_spec_object ("spell-navigator",
+ "Spell Navigator",
+ "",
+ GSPELL_TYPE_NAVIGATOR,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS));
+
+ /* Bind class to template */
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/gspell/checker-dialog.ui");
+ gtk_widget_class_bind_template_child_private (widget_class, GspellCheckerDialog,
misspelled_word_label);
+ gtk_widget_class_bind_template_child_private (widget_class, GspellCheckerDialog, word_entry);
+ gtk_widget_class_bind_template_child_private (widget_class, GspellCheckerDialog, check_word_button);
+ gtk_widget_class_bind_template_child_private (widget_class, GspellCheckerDialog, ignore_button);
+ gtk_widget_class_bind_template_child_private (widget_class, GspellCheckerDialog, ignore_all_button);
+ gtk_widget_class_bind_template_child_private (widget_class, GspellCheckerDialog, change_button);
+ gtk_widget_class_bind_template_child_private (widget_class, GspellCheckerDialog, change_all_button);
+ gtk_widget_class_bind_template_child_private (widget_class, GspellCheckerDialog, add_word_button);
+ gtk_widget_class_bind_template_child_private (widget_class, GspellCheckerDialog, suggestions_view);
+}
+
+static void
+word_entry_changed_handler (GtkEntry *word_entry,
+ GspellCheckerDialog *dialog)
+{
+ GspellCheckerDialogPrivate *priv;
+ gboolean sensitive;
+
+ priv = gspell_checker_dialog_get_instance_private (dialog);
+
+ sensitive = gtk_entry_get_text_length (word_entry) > 0;
+
+ gtk_widget_set_sensitive (priv->check_word_button, sensitive);
+ gtk_widget_set_sensitive (priv->change_button, sensitive);
+ gtk_widget_set_sensitive (priv->change_all_button, sensitive);
+}
+
+static void
+suggestions_selection_changed_handler (GtkTreeSelection *selection,
+ GspellCheckerDialog *dialog)
+{
+ GspellCheckerDialogPrivate *priv;
+ GtkTreeModel *model;
+ GtkTreeIter iter;
+ gchar *text;
+
+ priv = gspell_checker_dialog_get_instance_private (dialog);
+
+ if (!gtk_tree_selection_get_selected (selection, &model, &iter))
+ {
+ return;
+ }
+
+ gtk_tree_model_get (model, &iter,
+ COLUMN_SUGGESTION, &text,
+ -1);
+
+ gtk_entry_set_text (priv->word_entry, text);
+
+ g_free (text);
+}
+
+static void
+check_word_button_clicked_handler (GtkButton *button,
+ GspellCheckerDialog *dialog)
+{
+ GspellCheckerDialogPrivate *priv;
+ const gchar *word;
+ gboolean correctly_spelled;
+ GError *error = NULL;
+
+ priv = gspell_checker_dialog_get_instance_private (dialog);
+
+ g_return_if_fail (gtk_entry_get_text_length (priv->word_entry) > 0);
+
+ word = gtk_entry_get_text (priv->word_entry);
+
+ correctly_spelled = gspell_checker_check_word (priv->checker, word, -1, &error);
+
+ if (error != NULL)
+ {
+ show_error (dialog, error);
+ g_error_free (error);
+ return;
+ }
+
+ if (correctly_spelled)
+ {
+ GtkListStore *store;
+ GtkTreeIter iter;
+
+ clear_suggestions (dialog);
+
+ store = GTK_LIST_STORE (gtk_tree_view_get_model (priv->suggestions_view));
+
+ gtk_list_store_append (store, &iter);
+ gtk_list_store_set (store, &iter,
+ /* Translators: Displayed in the "Check
+ * Spelling" dialog if the current word
+ * isn't misspelled.
+ */
+ COLUMN_SUGGESTION, _("(correct spelling)"),
+ -1);
+
+ gtk_widget_set_sensitive (GTK_WIDGET (priv->suggestions_view), FALSE);
+ }
+ else
+ {
+ GSList *suggestions;
+
+ suggestions = gspell_checker_get_suggestions (priv->checker, word, -1);
+
+ set_suggestions (dialog, suggestions);
+
+ g_slist_free_full (suggestions, g_free);
+ }
+}
+
+static void
+add_word_button_clicked_handler (GtkButton *button,
+ GspellCheckerDialog *dialog)
+{
+ GspellCheckerDialogPrivate *priv;
+
+ priv = gspell_checker_dialog_get_instance_private (dialog);
+
+ g_return_if_fail (priv->misspelled_word != NULL);
+
+ gspell_checker_add_word_to_personal (priv->checker, priv->misspelled_word, -1);
+
+ goto_next (dialog);
+}
+
+static void
+ignore_button_clicked_handler (GtkButton *button,
+ GspellCheckerDialog *dialog)
+{
+ goto_next (dialog);
+}
+
+static void
+ignore_all_button_clicked_handler (GtkButton *button,
+ GspellCheckerDialog *dialog)
+{
+ GspellCheckerDialogPrivate *priv;
+
+ priv = gspell_checker_dialog_get_instance_private (dialog);
+
+ g_return_if_fail (priv->misspelled_word != NULL);
+
+ gspell_checker_add_word_to_session (priv->checker, priv->misspelled_word, -1);
+
+ goto_next (dialog);
+}
+
+static void
+change_button_clicked_handler (GtkButton *button,
+ GspellCheckerDialog *dialog)
+{
+ GspellCheckerDialogPrivate *priv;
+ const gchar *entry_text;
+ gchar *change_to;
+
+ priv = gspell_checker_dialog_get_instance_private (dialog);
+
+ g_return_if_fail (priv->misspelled_word != NULL);
+
+ entry_text = gtk_entry_get_text (priv->word_entry);
+ g_return_if_fail (entry_text != NULL);
+ g_return_if_fail (entry_text[0] != '\0');
+
+ change_to = g_strdup (entry_text);
+ gspell_checker_set_correction (priv->checker,
+ priv->misspelled_word, -1,
+ change_to, -1);
+
+ gspell_navigator_change (priv->navigator, priv->misspelled_word, change_to);
+ g_free (change_to);
+
+ goto_next (dialog);
+}
+
+/* double click on one of the suggestions is like clicking on "change" */
+static void
+suggestions_row_activated_handler (GtkTreeView *view,
+ GtkTreePath *path,
+ GtkTreeViewColumn *column,
+ GspellCheckerDialog *dialog)
+{
+ GspellCheckerDialogPrivate *priv;
+
+ priv = gspell_checker_dialog_get_instance_private (dialog);
+
+ change_button_clicked_handler (GTK_BUTTON (priv->change_button), dialog);
+}
+
+static void
+change_all_button_clicked_handler (GtkButton *button,
+ GspellCheckerDialog *dialog)
+{
+ GspellCheckerDialogPrivate *priv;
+ const gchar *entry_text;
+ gchar *change_to;
+
+ priv = gspell_checker_dialog_get_instance_private (dialog);
+
+ g_return_if_fail (priv->misspelled_word != NULL);
+
+ entry_text = gtk_entry_get_text (priv->word_entry);
+ g_return_if_fail (entry_text != NULL);
+ g_return_if_fail (entry_text[0] != '\0');
+
+ change_to = g_strdup (entry_text);
+ gspell_checker_set_correction (priv->checker,
+ priv->misspelled_word, -1,
+ change_to, -1);
+
+ gspell_navigator_change_all (priv->navigator, priv->misspelled_word, change_to);
+ g_free (change_to);
+
+ goto_next (dialog);
+}
+
+static void
+gspell_checker_dialog_init (GspellCheckerDialog *dialog)
+{
+ GspellCheckerDialogPrivate *priv;
+ GtkListStore *store;
+ GtkTreeViewColumn *column;
+ GtkCellRenderer *cell;
+ GtkTreeSelection *selection;
+
+ priv = gspell_checker_dialog_get_instance_private (dialog);
+
+ gtk_widget_init_template (GTK_WIDGET (dialog));
+
+ /* Suggestion list */
+ store = gtk_list_store_new (N_COLUMNS, G_TYPE_STRING);
+ gtk_tree_view_set_model (priv->suggestions_view, GTK_TREE_MODEL (store));
+ g_object_unref (store);
+
+ /* Add the suggestions column */
+ cell = gtk_cell_renderer_text_new ();
+ column = gtk_tree_view_column_new_with_attributes (_("Suggestions"), cell,
+ "text", COLUMN_SUGGESTION,
+ NULL);
+
+ gtk_tree_view_append_column (priv->suggestions_view, column);
+
+ gtk_tree_view_set_search_column (priv->suggestions_view, COLUMN_SUGGESTION);
+
+ selection = gtk_tree_view_get_selection (priv->suggestions_view);
+
+ gtk_tree_selection_set_mode (selection, GTK_SELECTION_SINGLE);
+
+ /* Connect signals */
+ g_signal_connect (priv->word_entry,
+ "changed",
+ G_CALLBACK (word_entry_changed_handler),
+ dialog);
+
+ g_signal_connect_object (selection,
+ "changed",
+ G_CALLBACK (suggestions_selection_changed_handler),
+ dialog,
+ 0);
+
+ g_signal_connect (priv->check_word_button,
+ "clicked",
+ G_CALLBACK (check_word_button_clicked_handler),
+ dialog);
+
+ g_signal_connect (priv->add_word_button,
+ "clicked",
+ G_CALLBACK (add_word_button_clicked_handler),
+ dialog);
+
+ g_signal_connect (priv->ignore_button,
+ "clicked",
+ G_CALLBACK (ignore_button_clicked_handler),
+ dialog);
+
+ g_signal_connect (priv->ignore_all_button,
+ "clicked",
+ G_CALLBACK (ignore_all_button_clicked_handler),
+ dialog);
+
+ g_signal_connect (priv->change_button,
+ "clicked",
+ G_CALLBACK (change_button_clicked_handler),
+ dialog);
+
+ g_signal_connect (priv->change_all_button,
+ "clicked",
+ G_CALLBACK (change_all_button_clicked_handler),
+ dialog);
+
+ g_signal_connect (priv->suggestions_view,
+ "row-activated",
+ G_CALLBACK (suggestions_row_activated_handler),
+ dialog);
+
+ gtk_widget_grab_default (priv->change_button);
+}
+
+/**
+ * gspell_checker_dialog_new:
+ * @parent: transient parent of the dialog.
+ * @navigator: the #GspellNavigator to use.
+ *
+ * Returns: a new #GspellCheckerDialog widget.
+ */
+GtkWidget *
+gspell_checker_dialog_new (GtkWindow *parent,
+ GspellNavigator *navigator)
+{
+ g_return_val_if_fail (GTK_IS_WINDOW (parent), NULL);
+ g_return_val_if_fail (GSPELL_IS_NAVIGATOR (navigator), NULL);
+
+ return g_object_new (GSPELL_TYPE_CHECKER_DIALOG,
+ "transient-for", parent,
+ "use-header-bar", TRUE,
+ "spell-navigator", navigator,
+ NULL);
+}
+
+/**
+ * gspell_checker_dialog_get_spell_navigator:
+ * @dialog: a #GspellCheckerDialog.
+ *
+ * Returns: (transfer none): the #GspellNavigator used.
+ */
+GspellNavigator *
+gspell_checker_dialog_get_spell_navigator (GspellCheckerDialog *dialog)
+{
+ GspellCheckerDialogPrivate *priv;
+
+ g_return_val_if_fail (GSPELL_IS_CHECKER_DIALOG (dialog), NULL);
+
+ priv = gspell_checker_dialog_get_instance_private (dialog);
+ return priv->navigator;
+}
+
+/* ex:set ts=8 noet: */
diff --git a/gspell/gspell-checker-dialog.h b/gspell/gspell-checker-dialog.h
new file mode 100644
index 0000000..7912573
--- /dev/null
+++ b/gspell/gspell-checker-dialog.h
@@ -0,0 +1,60 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2002 - Paolo Maggi
+ * Copyright 2015 - Sébastien Wilmet
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GSPELL_CHECKER_DIALOG_H
+#define GSPELL_CHECKER_DIALOG_H
+
+#if !defined (GSPELL_H_INSIDE) && !defined (GSPELL_COMPILATION)
+#error "Only <gspell/gspell.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+#include <gspell/gspell-navigator.h>
+#include <gspell/gspell-version.h>
+
+G_BEGIN_DECLS
+
+#define GSPELL_TYPE_CHECKER_DIALOG (gspell_checker_dialog_get_type ())
+
+GSPELL_AVAILABLE_IN_ALL
+G_DECLARE_DERIVABLE_TYPE (GspellCheckerDialog, gspell_checker_dialog,
+ GSPELL, CHECKER_DIALOG,
+ GtkDialog)
+
+struct _GspellCheckerDialogClass
+{
+ GtkDialogClass parent_class;
+
+ /* Padding for future expansion */
+ gpointer padding[8];
+};
+
+GSPELL_AVAILABLE_IN_ALL
+GtkWidget * gspell_checker_dialog_new (GtkWindow *parent,
+ GspellNavigator *navigator);
+
+GSPELL_AVAILABLE_IN_ALL
+GspellNavigator * gspell_checker_dialog_get_spell_navigator (GspellCheckerDialog *dialog);
+
+G_END_DECLS
+
+#endif /* GSPELL_CHECKER_DIALOG_H */
+
+/* ex:set ts=8 noet: */
diff --git a/gspell/gspell-checker-private.h b/gspell/gspell-checker-private.h
new file mode 100644
index 0000000..be766f1
--- /dev/null
+++ b/gspell/gspell-checker-private.h
@@ -0,0 +1,35 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2016 - Sébastien Wilmet
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GSPELL_CHECKER_PRIVATE_H
+#define GSPELL_CHECKER_PRIVATE_H
+
+#include "gspell-checker.h"
+
+G_BEGIN_DECLS
+
+G_GNUC_INTERNAL
+void _gspell_checker_force_set_language (GspellChecker *checker,
+ const GspellLanguage *language);
+
+G_END_DECLS
+
+#endif /* GSPELL_CHECKER_PRIVATE_H */
+
+/* ex:set ts=8 noet: */
diff --git a/gspell/gspell-checker.c b/gspell/gspell-checker.c
new file mode 100644
index 0000000..8314ce0
--- /dev/null
+++ b/gspell/gspell-checker.c
@@ -0,0 +1,667 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2002-2006 - Paolo Maggi
+ * Copyright 2015, 2016 - Sébastien Wilmet
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "gspell-checker.h"
+#include "gspell-checker-private.h"
+#include <glib/gi18n-lib.h>
+#include <string.h>
+#include "gspell-utils.h"
+
+#ifdef OS_OSX
+#include "gspell-osx.h"
+#endif
+
+/**
+ * SECTION:checker
+ * @Short_description: Spell checker
+ * @Title: GspellChecker
+ * @See_also: #GspellLanguage
+ *
+ * #GspellChecker is a spell checker.
+ *
+ * If the #GspellChecker:language property is %NULL, it means that no
+ * dictonaries are available, in which case the #GspellChecker is in a
+ * “disabled” (but allowed) state.
+ *
+ * gspell uses the [Enchant](https://abiword.github.io/enchant/) library. The
+ * use of Enchant is part of the gspell API, #GspellChecker exposes the
+ * EnchantDict with the gspell_checker_get_enchant_dict() function.
+ */
+
+typedef struct _GspellCheckerPrivate GspellCheckerPrivate;
+
+struct _GspellCheckerPrivate
+{
+ EnchantBroker *broker;
+ EnchantDict *dict;
+ const GspellLanguage *active_lang;
+};
+
+enum
+{
+ PROP_0,
+ PROP_LANGUAGE,
+};
+
+enum
+{
+ SIGNAL_WORD_ADDED_TO_PERSONAL,
+ SIGNAL_WORD_ADDED_TO_SESSION,
+ SIGNAL_SESSION_CLEARED,
+ LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL] = { 0 };
+
+G_DEFINE_TYPE_WITH_PRIVATE (GspellChecker, gspell_checker, G_TYPE_OBJECT)
+
+GQuark
+gspell_checker_error_quark (void)
+{
+ static GQuark quark = 0;
+
+ if (G_UNLIKELY (quark == 0))
+ {
+ quark = g_quark_from_static_string ("gspell-checker-error-quark");
+ }
+
+ return quark;
+}
+
+static void
+gspell_checker_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GspellChecker *checker = GSPELL_CHECKER (object);
+
+ switch (prop_id)
+ {
+ case PROP_LANGUAGE:
+ gspell_checker_set_language (checker, g_value_get_boxed (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gspell_checker_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GspellChecker *checker = GSPELL_CHECKER (object);
+
+ switch (prop_id)
+ {
+ case PROP_LANGUAGE:
+ g_value_set_boxed (value, gspell_checker_get_language (checker));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gspell_checker_finalize (GObject *object)
+{
+ GspellCheckerPrivate *priv;
+
+ priv = gspell_checker_get_instance_private (GSPELL_CHECKER (object));
+
+ if (priv->dict != NULL)
+ {
+ enchant_broker_free_dict (priv->broker, priv->dict);
+ }
+
+ if (priv->broker != NULL)
+ {
+ enchant_broker_free (priv->broker);
+ }
+
+ G_OBJECT_CLASS (gspell_checker_parent_class)->finalize (object);
+}
+
+static void
+gspell_checker_class_init (GspellCheckerClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->set_property = gspell_checker_set_property;
+ object_class->get_property = gspell_checker_get_property;
+ object_class->finalize = gspell_checker_finalize;
+
+ /**
+ * GspellChecker:language:
+ *
+ * The #GspellLanguage used.
+ */
+ g_object_class_install_property (object_class,
+ PROP_LANGUAGE,
+ g_param_spec_boxed ("language",
+ "Language",
+ "",
+ GSPELL_TYPE_LANGUAGE,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * GspellChecker::word-added-to-personal:
+ * @spell_checker: the #GspellChecker.
+ * @word: the added word.
+ *
+ * Emitted when a word is added to the personal dictionary.
+ */
+ signals[SIGNAL_WORD_ADDED_TO_PERSONAL] =
+ g_signal_new ("word-added-to-personal",
+ G_OBJECT_CLASS_TYPE (object_class),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GspellCheckerClass, word_added_to_personal),
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 1,
+ G_TYPE_STRING);
+
+ /**
+ * GspellChecker::word-added-to-session:
+ * @spell_checker: the #GspellChecker.
+ * @word: the added word.
+ *
+ * Emitted when a word is added to the session dictionary. See
+ * gspell_checker_add_word_to_session().
+ */
+ signals[SIGNAL_WORD_ADDED_TO_SESSION] =
+ g_signal_new ("word-added-to-session",
+ G_OBJECT_CLASS_TYPE (object_class),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GspellCheckerClass, word_added_to_session),
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 1,
+ G_TYPE_STRING);
+
+ /**
+ * GspellChecker::session-cleared:
+ * @spell_checker: the #GspellChecker.
+ *
+ * Emitted when the session dictionary is cleared.
+ */
+ signals[SIGNAL_SESSION_CLEARED] =
+ g_signal_new ("session-cleared",
+ G_OBJECT_CLASS_TYPE (object_class),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GspellCheckerClass, session_cleared),
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 0);
+}
+
+static void
+gspell_checker_init (GspellChecker *checker)
+{
+ GspellCheckerPrivate *priv;
+
+ priv = gspell_checker_get_instance_private (checker);
+
+ priv->broker = enchant_broker_init ();
+ priv->dict = NULL;
+ priv->active_lang = NULL;
+}
+
+/**
+ * gspell_checker_new:
+ * @language: (nullable): the #GspellLanguage to use, or %NULL.
+ *
+ * Creates a new #GspellChecker. If @language is %NULL, the default language is
+ * picked with gspell_language_get_default().
+ *
+ * Returns: a new #GspellChecker object.
+ */
+GspellChecker *
+gspell_checker_new (const GspellLanguage *language)
+{
+ return g_object_new (GSPELL_TYPE_CHECKER,
+ "language", language,
+ NULL);
+}
+
+static void
+create_new_dictionary (GspellChecker *checker)
+{
+ GspellCheckerPrivate *priv;
+ const gchar *language_code;
+ const gchar *app_name;
+
+ priv = gspell_checker_get_instance_private (checker);
+
+ if (priv->dict != NULL)
+ {
+ enchant_broker_free_dict (priv->broker, priv->dict);
+ priv->dict = NULL;
+ }
+
+ if (priv->active_lang == NULL)
+ {
+ return;
+ }
+
+ language_code = gspell_language_get_code (priv->active_lang);
+ priv->dict = enchant_broker_request_dict (priv->broker, language_code);
+
+ if (priv->dict == NULL)
+ {
+ /* Should never happen, no need to return a GError. */
+ g_warning ("Impossible to create an Enchant dictionary for the language code '%s'.",
+ language_code);
+
+ priv->active_lang = NULL;
+ return;
+ }
+
+ app_name = g_get_application_name ();
+ gspell_checker_add_word_to_session (checker, app_name, -1);
+}
+
+/* Used for unit tests. Useful to force a NULL language. */
+void
+_gspell_checker_force_set_language (GspellChecker *checker,
+ const GspellLanguage *language)
+{
+ GspellCheckerPrivate *priv;
+
+ g_return_if_fail (GSPELL_IS_CHECKER (checker));
+
+ priv = gspell_checker_get_instance_private (checker);
+
+ if (priv->active_lang != language)
+ {
+ priv->active_lang = language;
+ create_new_dictionary (checker);
+ g_object_notify (G_OBJECT (checker), "language");
+ }
+}
+
+/**
+ * gspell_checker_set_language:
+ * @checker: a #GspellChecker.
+ * @language: (nullable): the #GspellLanguage to use, or %NULL.
+ *
+ * Sets the language to use for the spell checking. If @language is %NULL, the
+ * default language is picked with gspell_language_get_default().
+ */
+void
+gspell_checker_set_language (GspellChecker *checker,
+ const GspellLanguage *language)
+{
+ g_return_if_fail (GSPELL_IS_CHECKER (checker));
+
+ if (language == NULL)
+ {
+ language = gspell_language_get_default ();
+ }
+
+ _gspell_checker_force_set_language (checker, language);
+}
+
+/**
+ * gspell_checker_get_language:
+ * @checker: a #GspellChecker.
+ *
+ * Returns: (nullable): the #GspellLanguage currently used, or %NULL
+ * if no dictionaries are available.
+ */
+const GspellLanguage *
+gspell_checker_get_language (GspellChecker *checker)
+{
+ GspellCheckerPrivate *priv;
+
+ g_return_val_if_fail (GSPELL_IS_CHECKER (checker), NULL);
+
+ priv = gspell_checker_get_instance_private (checker);
+
+ return priv->active_lang;
+}
+
+/**
+ * gspell_checker_check_word:
+ * @checker: a #GspellChecker.
+ * @word: the word to check.
+ * @word_length: the byte length of @word, or -1 if @word is nul-terminated.
+ * @error: (out) (optional): a location to a %NULL #GError, or %NULL.
+ *
+ * If the #GspellChecker:language is %NULL, i.e. when no dictonaries are
+ * available, this function returns %TRUE to limit the damage.
+ *
+ * Returns: %TRUE if @word is correctly spelled, %FALSE otherwise.
+ */
+gboolean
+gspell_checker_check_word (GspellChecker *checker,
+ const gchar *word,
+ gssize word_length,
+ GError **error)
+{
+ GspellCheckerPrivate *priv;
+ gint enchant_result;
+ gboolean correctly_spelled;
+ gchar *sanitized_word;
+
+ g_return_val_if_fail (GSPELL_IS_CHECKER (checker), FALSE);
+ g_return_val_if_fail (word != NULL, FALSE);
+ g_return_val_if_fail (word_length >= -1, FALSE);
+ g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+
+ priv = gspell_checker_get_instance_private (checker);
+
+ if (priv->dict == NULL)
+ {
+ return TRUE;
+ }
+
+ if (_gspell_utils_is_number (word, word_length))
+ {
+ return TRUE;
+ }
+
+ if (_gspell_utils_str_to_ascii_apostrophe (word, word_length, &sanitized_word))
+ {
+ enchant_result = enchant_dict_check (priv->dict, sanitized_word, -1);
+ g_free (sanitized_word);
+ }
+ else
+ {
+ enchant_result = enchant_dict_check (priv->dict, word, word_length);
+ }
+
+ correctly_spelled = enchant_result == 0;
+
+ if (enchant_result < 0)
+ {
+ gchar *nul_terminated_word;
+
+ if (word_length == -1)
+ {
+ word_length = strlen (word);
+ }
+
+ nul_terminated_word = g_strndup (word, word_length);
+
+ g_set_error (error,
+ GSPELL_CHECKER_ERROR,
+ GSPELL_CHECKER_ERROR_DICTIONARY,
+ _("Error when checking the spelling of word “%s”: %s"),
+ nul_terminated_word,
+ enchant_dict_get_error (priv->dict));
+
+ g_free (nul_terminated_word);
+ }
+
+ return correctly_spelled;
+}
+
+/**
+ * gspell_checker_get_suggestions:
+ * @checker: a #GspellChecker.
+ * @word: a misspelled word.
+ * @word_length: the byte length of @word, or -1 if @word is nul-terminated.
+ *
+ * Gets the suggestions for @word. Free the return value with
+ * g_slist_free_full(suggestions, g_free).
+ *
+ * Returns: (transfer full) (element-type utf8): the list of suggestions.
+ */
+GSList *
+gspell_checker_get_suggestions (GspellChecker *checker,
+ const gchar *word,
+ gssize word_length)
+{
+ GspellCheckerPrivate *priv;
+ gchar *sanitized_word;
+ gchar **suggestions;
+ GSList *suggestions_list = NULL;
+ gint i;
+
+ g_return_val_if_fail (GSPELL_IS_CHECKER (checker), NULL);
+ g_return_val_if_fail (word != NULL, NULL);
+ g_return_val_if_fail (word_length >= -1, NULL);
+
+ priv = gspell_checker_get_instance_private (checker);
+
+ if (priv->dict == NULL)
+ {
+ return NULL;
+ }
+
+ if (_gspell_utils_str_to_ascii_apostrophe (word, word_length, &sanitized_word))
+ {
+ suggestions = enchant_dict_suggest (priv->dict, sanitized_word, -1, NULL);
+ g_free (sanitized_word);
+ }
+ else
+ {
+ suggestions = enchant_dict_suggest (priv->dict, word, word_length, NULL);
+ }
+
+ if (suggestions == NULL)
+ {
+ return NULL;
+ }
+
+ for (i = 0; suggestions[i] != NULL; i++)
+ {
+ suggestions_list = g_slist_prepend (suggestions_list, suggestions[i]);
+ }
+
+ /* The array/list elements will be freed by the caller. */
+ g_free (suggestions);
+
+ return g_slist_reverse (suggestions_list);
+}
+
+/**
+ * gspell_checker_add_word_to_personal:
+ * @checker: a #GspellChecker.
+ * @word: a word.
+ * @word_length: the byte length of @word, or -1 if @word is nul-terminated.
+ *
+ * Adds a word to the personal dictionary. It is typically saved in the user's
+ * home directory.
+ */
+void
+gspell_checker_add_word_to_personal (GspellChecker *checker,
+ const gchar *word,
+ gssize word_length)
+{
+ GspellCheckerPrivate *priv;
+
+ g_return_if_fail (GSPELL_IS_CHECKER (checker));
+ g_return_if_fail (word != NULL);
+ g_return_if_fail (word_length >= -1);
+
+ priv = gspell_checker_get_instance_private (checker);
+
+ if (priv->dict == NULL)
+ {
+ return;
+ }
+
+ enchant_dict_add (priv->dict, word, word_length);
+
+ if (word_length == -1)
+ {
+ g_signal_emit (G_OBJECT (checker),
+ signals[SIGNAL_WORD_ADDED_TO_PERSONAL], 0,
+ word);
+ }
+ else
+ {
+ gchar *nul_terminated_word = g_strndup (word, word_length);
+
+ g_signal_emit (G_OBJECT (checker),
+ signals[SIGNAL_WORD_ADDED_TO_PERSONAL], 0,
+ nul_terminated_word);
+
+ g_free (nul_terminated_word);
+ }
+}
+
+/**
+ * gspell_checker_add_word_to_session:
+ * @checker: a #GspellChecker.
+ * @word: a word.
+ * @word_length: the byte length of @word, or -1 if @word is nul-terminated.
+ *
+ * Adds a word to the session dictionary. Each #GspellChecker instance has a
+ * different session dictionary. The session dictionary is lost when the
+ * #GspellChecker:language property changes or when @checker is destroyed or
+ * when gspell_checker_clear_session() is called.
+ *
+ * This function is typically called for an “Ignore All” action.
+ */
+void
+gspell_checker_add_word_to_session (GspellChecker *checker,
+ const gchar *word,
+ gssize word_length)
+{
+ GspellCheckerPrivate *priv;
+
+ g_return_if_fail (GSPELL_IS_CHECKER (checker));
+ g_return_if_fail (word != NULL);
+ g_return_if_fail (word_length >= -1);
+
+ priv = gspell_checker_get_instance_private (checker);
+
+ if (priv->dict == NULL)
+ {
+ return;
+ }
+
+ enchant_dict_add_to_session (priv->dict, word, word_length);
+
+ if (word_length == -1)
+ {
+ g_signal_emit (G_OBJECT (checker),
+ signals[SIGNAL_WORD_ADDED_TO_SESSION], 0,
+ word);
+ }
+ else
+ {
+ gchar *nul_terminated_word = g_strndup (word, word_length);
+
+ g_signal_emit (G_OBJECT (checker),
+ signals[SIGNAL_WORD_ADDED_TO_SESSION], 0,
+ nul_terminated_word);
+
+ g_free (nul_terminated_word);
+ }
+}
+
+/**
+ * gspell_checker_clear_session:
+ * @checker: a #GspellChecker.
+ *
+ * Clears the session dictionary.
+ */
+void
+gspell_checker_clear_session (GspellChecker *checker)
+{
+ g_return_if_fail (GSPELL_IS_CHECKER (checker));
+
+ /* Free and re-request dictionary. */
+ create_new_dictionary (checker);
+
+ g_signal_emit (G_OBJECT (checker), signals[SIGNAL_SESSION_CLEARED], 0);
+}
+
+/**
+ * gspell_checker_set_correction:
+ * @checker: a #GspellChecker.
+ * @word: a word.
+ * @word_length: the byte length of @word, or -1 if @word is nul-terminated.
+ * @replacement: the replacement word.
+ * @replacement_length: the byte length of @replacement, or -1 if @replacement
+ * is nul-terminated.
+ *
+ * Informs the spell checker that @word is replaced/corrected by @replacement.
+ */
+void
+gspell_checker_set_correction (GspellChecker *checker,
+ const gchar *word,
+ gssize word_length,
+ const gchar *replacement,
+ gssize replacement_length)
+{
+ GspellCheckerPrivate *priv;
+
+ g_return_if_fail (GSPELL_IS_CHECKER (checker));
+ g_return_if_fail (word != NULL);
+ g_return_if_fail (word_length >= -1);
+ g_return_if_fail (replacement != NULL);
+ g_return_if_fail (replacement_length >= -1);
+
+ priv = gspell_checker_get_instance_private (checker);
+
+ if (priv->dict == NULL)
+ {
+ return;
+ }
+
+ enchant_dict_store_replacement (priv->dict,
+ word, word_length,
+ replacement, replacement_length);
+}
+
+/**
+ * gspell_checker_get_enchant_dict: (skip)
+ * @checker: a #GspellChecker.
+ *
+ * Gets the EnchantDict currently used by @checker. It permits to extend
+ * #GspellChecker with more features. Note that by doing so, the other classes
+ * in gspell may no longer work well.
+ *
+ * #GspellChecker re-creates a new EnchantDict when the #GspellChecker:language
+ * is changed and when the session is cleared.
+ *
+ * Returns: (transfer none) (nullable): the EnchantDict currently used by
+ * @checker.
+ * Since: 1.6
+ */
+EnchantDict *
+gspell_checker_get_enchant_dict (GspellChecker *checker)
+{
+ GspellCheckerPrivate *priv;
+
+ g_return_val_if_fail (GSPELL_IS_CHECKER (checker), NULL);
+
+ priv = gspell_checker_get_instance_private (checker);
+ return priv->dict;
+}
+
+/* ex:set ts=8 noet: */
diff --git a/gspell/gspell-checker.h b/gspell/gspell-checker.h
new file mode 100644
index 0000000..bab3617
--- /dev/null
+++ b/gspell/gspell-checker.h
@@ -0,0 +1,134 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2002-2006 - Paolo Maggi
+ * Copyright 2015 - Sébastien Wilmet
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GSPELL_CHECKER_H
+#define GSPELL_CHECKER_H
+
+#if !defined (GSPELL_H_INSIDE) && !defined (GSPELL_COMPILATION)
+#error "Only <gspell/gspell.h> can be included directly."
+#endif
+
+#include <glib-object.h>
+#include <enchant.h>
+#include <gspell/gspell-language.h>
+#include <gspell/gspell-version.h>
+
+G_BEGIN_DECLS
+
+#define GSPELL_TYPE_CHECKER (gspell_checker_get_type ())
+
+GSPELL_AVAILABLE_IN_ALL
+G_DECLARE_DERIVABLE_TYPE (GspellChecker, gspell_checker,
+ GSPELL, CHECKER,
+ GObject)
+
+/**
+ * GSPELL_CHECKER_ERROR:
+ *
+ * Error domain for the spell checker. Errors in this domain will be from the
+ * #GspellCheckerError enumeration. See #GError for more information on
+ * error domains.
+ */
+#define GSPELL_CHECKER_ERROR (gspell_checker_error_quark ())
+
+/**
+ * GspellCheckerError:
+ * @GSPELL_CHECKER_ERROR_DICTIONARY: dictionary error.
+ * @GSPELL_CHECKER_ERROR_NO_LANGUAGE_SET: no language set.
+ *
+ * An error code used with %GSPELL_CHECKER_ERROR in a #GError returned
+ * from a spell-checker-related function.
+ */
+typedef enum _GspellCheckerError
+{
+ GSPELL_CHECKER_ERROR_DICTIONARY,
+ GSPELL_CHECKER_ERROR_NO_LANGUAGE_SET,
+} GspellCheckerError;
+
+struct _GspellCheckerClass
+{
+ GObjectClass parent_class;
+
+ /* Signals */
+ void (* word_added_to_personal) (GspellChecker *checker,
+ const gchar *word);
+
+ void (* word_added_to_session) (GspellChecker *checker,
+ const gchar *word);
+
+ void (* session_cleared) (GspellChecker *checker);
+
+ /* Padding for future expansion */
+ gpointer padding[12];
+};
+
+GSPELL_AVAILABLE_IN_ALL
+GQuark gspell_checker_error_quark (void);
+
+GSPELL_AVAILABLE_IN_ALL
+GspellChecker * gspell_checker_new (const GspellLanguage *language);
+
+GSPELL_AVAILABLE_IN_ALL
+void gspell_checker_set_language (GspellChecker *checker,
+ const GspellLanguage *language);
+
+GSPELL_AVAILABLE_IN_ALL
+const GspellLanguage *
+ gspell_checker_get_language (GspellChecker *checker);
+
+GSPELL_AVAILABLE_IN_ALL
+gboolean gspell_checker_check_word (GspellChecker *checker,
+ const gchar *word,
+ gssize word_length,
+ GError **error);
+
+GSPELL_AVAILABLE_IN_ALL
+GSList * gspell_checker_get_suggestions (GspellChecker *checker,
+ const gchar *word,
+ gssize word_length);
+
+GSPELL_AVAILABLE_IN_ALL
+void gspell_checker_add_word_to_personal (GspellChecker *checker,
+ const gchar *word,
+ gssize word_length);
+
+GSPELL_AVAILABLE_IN_ALL
+void gspell_checker_add_word_to_session (GspellChecker *checker,
+ const gchar *word,
+ gssize word_length);
+
+GSPELL_AVAILABLE_IN_ALL
+void gspell_checker_clear_session (GspellChecker *checker);
+
+GSPELL_AVAILABLE_IN_ALL
+void gspell_checker_set_correction (GspellChecker *checker,
+ const gchar *word,
+ gssize word_length,
+ const gchar *replacement,
+ gssize replacement_length);
+
+GSPELL_AVAILABLE_IN_1_6
+EnchantDict * gspell_checker_get_enchant_dict (GspellChecker *checker);
+
+G_END_DECLS
+
+#endif /* GSPELL_CHECKER_H */
+
+/* ex:set ts=8 noet: */
diff --git a/gspell/gspell-context-menu.c b/gspell/gspell-context-menu.c
new file mode 100644
index 0000000..1e21010
--- /dev/null
+++ b/gspell/gspell-context-menu.c
@@ -0,0 +1,347 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2016 - Sébastien Wilmet
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "gspell-context-menu.h"
+#include <glib/gi18n-lib.h>
+
+#define LANGUAGE_DATA_KEY "gspell-language-data-key"
+#define SUGGESTION_DATA_KEY "gspell-suggestion-data-key"
+
+typedef struct _LanguageData LanguageData;
+typedef struct _SuggestionData SuggestionData;
+
+struct _LanguageData
+{
+ const GspellLanguage *lang;
+ GspellLanguageActivatedCallback callback;
+ gpointer user_data;
+};
+
+struct _SuggestionData
+{
+ GspellChecker *checker;
+ gchar *misspelled_word;
+
+ gchar *suggested_word;
+ GspellSuggestionActivatedCallback callback;
+ gpointer user_data;
+};
+
+static void
+suggestion_data_free (gpointer data)
+{
+ SuggestionData *suggestion_data = data;
+
+ if (suggestion_data != NULL)
+ {
+ g_clear_object (&suggestion_data->checker);
+ g_free (suggestion_data->misspelled_word);
+ g_free (suggestion_data->suggested_word);
+ g_free (suggestion_data);
+ }
+}
+
+static void
+activate_language_cb (GtkWidget *menu_item)
+{
+ LanguageData *data;
+
+ data = g_object_get_data (G_OBJECT (menu_item), LANGUAGE_DATA_KEY);
+ g_return_if_fail (data != NULL);
+
+ if (data->callback != NULL)
+ {
+ data->callback (data->lang, data->user_data);
+ }
+}
+
+static GtkWidget *
+get_language_menu (const GspellLanguage *current_language,
+ GspellLanguageActivatedCallback callback,
+ gpointer user_data)
+{
+ GtkWidget *menu;
+ const GList *languages;
+ const GList *l;
+
+ menu = gtk_menu_new ();
+
+ languages = gspell_language_get_available ();
+ for (l = languages; l != NULL; l = l->next)
+ {
+ const GspellLanguage *lang = l->data;
+ const gchar *lang_name;
+ GtkWidget *menu_item;
+ LanguageData *data;
+
+ lang_name = gspell_language_get_name (lang);
+
+ if (lang == current_language)
+ {
+ /* Do not create a group. Just mark the current language
+ * as active.
+ *
+ * With a group, the first language in the list gets
+ * activated, which changes the GspellChecker language
+ * before we arrive to the current_language.
+ *
+ * Also, having a bullet only for the current_language is
+ * sufficient (to be like in Firefox), the menu is
+ * anyway ephemeral. No need to have an empty bullet for
+ * all the other languages.
+ */
+ menu_item = gtk_radio_menu_item_new_with_label (NULL, lang_name);
+ gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (menu_item), TRUE);
+ }
+ else
+ {
+ menu_item = gtk_menu_item_new_with_label (lang_name);
+ }
+
+ gtk_menu_shell_append (GTK_MENU_SHELL (menu), menu_item);
+
+ data = g_new0 (LanguageData, 1);
+ data->lang = lang;
+ data->callback = callback;
+ data->user_data = user_data;
+
+ g_object_set_data_full (G_OBJECT (menu_item),
+ LANGUAGE_DATA_KEY,
+ data,
+ g_free);
+
+ g_signal_connect (menu_item,
+ "activate",
+ G_CALLBACK (activate_language_cb),
+ NULL);
+ }
+
+ return menu;
+}
+
+GtkMenuItem *
+_gspell_context_menu_get_language_menu_item (const GspellLanguage *current_language,
+ GspellLanguageActivatedCallback callback,
+ gpointer user_data)
+{
+ GtkWidget *lang_menu;
+ GtkMenuItem *menu_item;
+
+ lang_menu = get_language_menu (current_language, callback, user_data);
+
+ menu_item = GTK_MENU_ITEM (gtk_menu_item_new_with_mnemonic (_("_Language")));
+ gtk_menu_item_set_submenu (menu_item, lang_menu);
+ gtk_widget_show_all (GTK_WIDGET (menu_item));
+
+ return menu_item;
+}
+
+static void
+activate_suggestion_cb (GtkWidget *menu_item)
+{
+ SuggestionData *data;
+
+ data = g_object_get_data (G_OBJECT (menu_item), SUGGESTION_DATA_KEY);
+ g_return_if_fail (data != NULL);
+
+ if (data->callback != NULL)
+ {
+ data->callback (data->suggested_word, data->user_data);
+ }
+}
+
+static void
+ignore_all_cb (GtkWidget *menu_item)
+{
+ SuggestionData *data;
+
+ data = g_object_get_data (G_OBJECT (menu_item), SUGGESTION_DATA_KEY);
+ g_return_if_fail (data != NULL);
+
+ gspell_checker_add_word_to_session (data->checker,
+ data->misspelled_word,
+ -1);
+}
+
+static void
+add_to_dictionary_cb (GtkWidget *menu_item)
+{
+ SuggestionData *data;
+
+ data = g_object_get_data (G_OBJECT (menu_item), SUGGESTION_DATA_KEY);
+ g_return_if_fail (data != NULL);
+
+ gspell_checker_add_word_to_personal (data->checker,
+ data->misspelled_word,
+ -1);
+}
+
+static GtkWidget *
+get_suggestion_menu (GspellChecker *checker,
+ const gchar *misspelled_word,
+ GspellSuggestionActivatedCallback callback,
+ gpointer user_data)
+{
+ GtkWidget *top_menu;
+ GtkWidget *menu_item;
+ GSList *suggestions = NULL;
+ SuggestionData *data;
+
+ top_menu = gtk_menu_new ();
+
+ suggestions = gspell_checker_get_suggestions (checker, misspelled_word, -1);
+
+ if (suggestions == NULL)
+ {
+ /* No suggestions. Put something in the menu anyway... */
+ menu_item = gtk_menu_item_new_with_label (_("(no suggested words)"));
+ gtk_widget_set_sensitive (menu_item, FALSE);
+ gtk_menu_shell_prepend (GTK_MENU_SHELL (top_menu), menu_item);
+ }
+ else
+ {
+ GtkWidget *menu = top_menu;
+ gint count = 0;
+ GSList *l;
+
+ /* Build a set of menus with suggestions. */
+ for (l = suggestions; l != NULL; l = l->next)
+ {
+ gchar *suggested_word = l->data;
+ GtkWidget *label;
+ gchar *label_text;
+
+ if (count == 10)
+ {
+ /* Separator */
+ menu_item = gtk_separator_menu_item_new ();
+ gtk_menu_shell_append (GTK_MENU_SHELL (menu), menu_item);
+
+ menu_item = gtk_menu_item_new_with_mnemonic (_("_More…"));
+ gtk_menu_shell_append (GTK_MENU_SHELL (menu), menu_item);
+
+ menu = gtk_menu_new ();
+ gtk_menu_item_set_submenu (GTK_MENU_ITEM (menu_item), menu);
+ count = 0;
+ }
+
+ label_text = g_strdup_printf ("<b>%s</b>", suggested_word);
+
+ label = gtk_label_new (label_text);
+ gtk_label_set_use_markup (GTK_LABEL (label), TRUE);
+ gtk_widget_set_halign (label, GTK_ALIGN_START);
+
+ menu_item = gtk_menu_item_new ();
+ gtk_container_add (GTK_CONTAINER (menu_item), label);
+ gtk_menu_shell_append (GTK_MENU_SHELL (menu), menu_item);
+
+ data = g_new0 (SuggestionData, 1);
+ data->suggested_word = g_strdup (suggested_word);
+ data->callback = callback;
+ data->user_data = user_data;
+
+ g_object_set_data_full (G_OBJECT (menu_item),
+ SUGGESTION_DATA_KEY,
+ data,
+ suggestion_data_free);
+
+ g_signal_connect (menu_item,
+ "activate",
+ G_CALLBACK (activate_suggestion_cb),
+ NULL);
+
+ g_free (label_text);
+ count++;
+ }
+ }
+
+ g_slist_free_full (suggestions, g_free);
+
+ /* Separator */
+ menu_item = gtk_separator_menu_item_new ();
+ gtk_menu_shell_append (GTK_MENU_SHELL (top_menu), menu_item);
+
+ /* Ignore all */
+ menu_item = gtk_menu_item_new_with_mnemonic (_("_Ignore All"));
+ gtk_menu_shell_append (GTK_MENU_SHELL (top_menu), menu_item);
+
+ data = g_new0 (SuggestionData, 1);
+ data->checker = g_object_ref (checker);
+ data->misspelled_word = g_strdup (misspelled_word);
+
+ g_object_set_data_full (G_OBJECT (menu_item),
+ SUGGESTION_DATA_KEY,
+ data,
+ suggestion_data_free);
+
+ g_signal_connect (menu_item,
+ "activate",
+ G_CALLBACK (ignore_all_cb),
+ NULL);
+
+ /* Add to Dictionary */
+ menu_item = gtk_menu_item_new_with_mnemonic (_("_Add"));
+ gtk_menu_shell_append (GTK_MENU_SHELL (top_menu), menu_item);
+
+ data = g_new0 (SuggestionData, 1);
+ data->checker = g_object_ref (checker);
+ data->misspelled_word = g_strdup (misspelled_word);
+
+ g_object_set_data_full (G_OBJECT (menu_item),
+ SUGGESTION_DATA_KEY,
+ data,
+ suggestion_data_free);
+
+ g_signal_connect (menu_item,
+ "activate",
+ G_CALLBACK (add_to_dictionary_cb),
+ NULL);
+
+ return top_menu;
+}
+
+GtkMenuItem *
+_gspell_context_menu_get_suggestions_menu_item (GspellChecker *checker,
+ const gchar *misspelled_word,
+ GspellSuggestionActivatedCallback callback,
+ gpointer user_data)
+{
+ GtkWidget *suggestion_menu;
+ GtkMenuItem *menu_item;
+
+ g_return_val_if_fail (GSPELL_IS_CHECKER (checker), NULL);
+ g_return_val_if_fail (misspelled_word != NULL, NULL);
+
+ suggestion_menu = get_suggestion_menu (checker,
+ misspelled_word,
+ callback,
+ user_data);
+
+ menu_item = GTK_MENU_ITEM (gtk_menu_item_new_with_mnemonic (_("_Spelling Suggestions…")));
+ gtk_menu_item_set_submenu (menu_item, suggestion_menu);
+ gtk_widget_show_all (GTK_WIDGET (menu_item));
+
+ return menu_item;
+}
+
+/* ex:set ts=8 noet: */
diff --git a/gspell/gspell-context-menu.h b/gspell/gspell-context-menu.h
new file mode 100644
index 0000000..ee2e430
--- /dev/null
+++ b/gspell/gspell-context-menu.h
@@ -0,0 +1,50 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2016 - Sébastien Wilmet
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GSPELL_CONTEXT_MENU_H
+#define GSPELL_CONTEXT_MENU_H
+
+#include <gtk/gtk.h>
+#include "gspell-checker.h"
+#include "gspell-language.h"
+
+G_BEGIN_DECLS
+
+typedef void (*GspellLanguageActivatedCallback) (const GspellLanguage *lang,
+ gpointer user_data);
+
+typedef void (*GspellSuggestionActivatedCallback) (const gchar *suggested_word,
+ gpointer user_data);
+
+G_GNUC_INTERNAL
+GtkMenuItem * _gspell_context_menu_get_language_menu_item (const GspellLanguage
*current_language,
+ GspellLanguageActivatedCallback callback,
+ gpointer user_data);
+
+G_GNUC_INTERNAL
+GtkMenuItem * _gspell_context_menu_get_suggestions_menu_item (GspellChecker *checker,
+ const gchar
*misspelled_word,
+ GspellSuggestionActivatedCallback callback,
+ gpointer
user_data);
+
+G_END_DECLS
+
+#endif /* GSPELL_CONTEXT_MENU_H */
+
+/* ex:set ts=8 noet: */
diff --git a/gspell/gspell-current-word-policy.c b/gspell/gspell-current-word-policy.c
new file mode 100644
index 0000000..8b9542e
--- /dev/null
+++ b/gspell/gspell-current-word-policy.c
@@ -0,0 +1,248 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2016 - Sébastien Wilmet
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "gspell-current-word-policy.h"
+
+/* An object that decides whether to check the current word. When a word is
+ * being typed, it should not be spell-checked, because it would be annoying to
+ * see the red wavy underline appearing and disappearing constantly.
+ *
+ * You need to feed the object with events, and get the result with
+ * _gspell_current_word_policy_get_check_current_word().
+ */
+
+typedef struct _GspellCurrentWordPolicyPrivate GspellCurrentWordPolicyPrivate;
+
+struct _GspellCurrentWordPolicyPrivate
+{
+ guint check_current_word : 1;
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (GspellCurrentWordPolicy, _gspell_current_word_policy, G_TYPE_OBJECT)
+
+static void
+_gspell_current_word_policy_dispose (GObject *object)
+{
+
+ G_OBJECT_CLASS (_gspell_current_word_policy_parent_class)->dispose (object);
+}
+
+static void
+_gspell_current_word_policy_finalize (GObject *object)
+{
+
+ G_OBJECT_CLASS (_gspell_current_word_policy_parent_class)->finalize (object);
+}
+
+static void
+_gspell_current_word_policy_class_init (GspellCurrentWordPolicyClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->dispose = _gspell_current_word_policy_dispose;
+ object_class->finalize = _gspell_current_word_policy_finalize;
+}
+
+static void
+_gspell_current_word_policy_init (GspellCurrentWordPolicy *policy)
+{
+ GspellCurrentWordPolicyPrivate *priv;
+
+ priv = _gspell_current_word_policy_get_instance_private (policy);
+ priv->check_current_word = TRUE;
+}
+
+GspellCurrentWordPolicy *
+_gspell_current_word_policy_new (void)
+{
+ return g_object_new (GSPELL_TYPE_CURRENT_WORD_POLICY, NULL);
+}
+
+gboolean
+_gspell_current_word_policy_get_check_current_word (GspellCurrentWordPolicy *policy)
+{
+ GspellCurrentWordPolicyPrivate *priv;
+
+ g_return_val_if_fail (GSPELL_IS_CURRENT_WORD_POLICY (policy), TRUE);
+
+ priv = _gspell_current_word_policy_get_instance_private (policy);
+
+ return priv->check_current_word;
+}
+
+/* For other events, it's better to use the more specific feed functions if
+ * possible.
+ */
+void
+_gspell_current_word_policy_set_check_current_word (GspellCurrentWordPolicy *policy,
+ gboolean check_current_word)
+{
+ GspellCurrentWordPolicyPrivate *priv;
+
+ g_return_if_fail (GSPELL_IS_CURRENT_WORD_POLICY (policy));
+
+ priv = _gspell_current_word_policy_get_instance_private (policy);
+
+ priv->check_current_word = check_current_word != FALSE;
+}
+
+/* On GspellChecker::session-cleared signal. */
+void
+_gspell_current_word_policy_session_cleared (GspellCurrentWordPolicy *policy)
+{
+ g_return_if_fail (GSPELL_IS_CURRENT_WORD_POLICY (policy));
+
+ _gspell_current_word_policy_set_check_current_word (policy, TRUE);
+}
+
+/* On GspellChecker::notify::language signal. */
+void
+_gspell_current_word_policy_language_changed (GspellCurrentWordPolicy *policy)
+{
+ g_return_if_fail (GSPELL_IS_CURRENT_WORD_POLICY (policy));
+
+ _gspell_current_word_policy_set_check_current_word (policy, TRUE);
+}
+
+/* When another GspellChecker object is used. */
+void
+_gspell_current_word_policy_checker_changed (GspellCurrentWordPolicy *policy)
+{
+ g_return_if_fail (GSPELL_IS_CURRENT_WORD_POLICY (policy));
+
+ _gspell_current_word_policy_set_check_current_word (policy, TRUE);
+}
+
+void
+_gspell_current_word_policy_cursor_moved (GspellCurrentWordPolicy *policy)
+{
+ g_return_if_fail (GSPELL_IS_CURRENT_WORD_POLICY (policy));
+
+ _gspell_current_word_policy_set_check_current_word (policy, TRUE);
+}
+
+/* After a text insertion. */
+void
+_gspell_current_word_policy_several_chars_inserted (GspellCurrentWordPolicy *policy)
+{
+ g_return_if_fail (GSPELL_IS_CURRENT_WORD_POLICY (policy));
+
+ /* If more than one character is inserted, it's probably not a normal
+ * keypress, e.g. a clipboard paste or DND. So it's better to check the
+ * current word in that case, to know ASAP if the word is correctly
+ * spelled.
+ */
+ _gspell_current_word_policy_set_check_current_word (policy, TRUE);
+}
+
+/* After a text insertion. */
+void
+_gspell_current_word_policy_single_char_inserted (GspellCurrentWordPolicy *policy,
+ gunichar ch,
+ gboolean empty_selection,
+ gboolean at_cursor_pos)
+{
+ g_return_if_fail (GSPELL_IS_CURRENT_WORD_POLICY (policy));
+
+ /* If e.g. a space or punctuation is inserted, we want to check the
+ * current word, since in that case we are not editing the current word.
+ * Maybe a word has been split in two, in which case the word on the
+ * left will anyway be checked, so it's better to know directly whether
+ * the word on the right is correctly spelled as well, so we know if we
+ * need to edit it or not.
+ * If there is a selection, it means that the text was inserted
+ * programmatically, so the user is not editing the current word
+ * manually.
+ */
+ if (g_unichar_isalnum (ch) &&
+ empty_selection &&
+ at_cursor_pos)
+ {
+ _gspell_current_word_policy_set_check_current_word (policy, FALSE);
+ }
+ else
+ {
+ _gspell_current_word_policy_set_check_current_word (policy, TRUE);
+ }
+}
+
+/* Before a text deletion.
+ *
+ * "start" refers to the start of the deletion.
+ * "end" refers to the end of the deletion.
+ * It is assumed that start < end.
+ *
+ * "inside word" and "ends word" have the same semantics as
+ * gtk_text_iter_inside_word() and gtk_text_iter_ends_word(), but custom word
+ * boundaries can be used.
+ */
+void
+_gspell_current_word_policy_text_deleted (GspellCurrentWordPolicy *policy,
+ gboolean empty_selection,
+ gboolean spans_several_lines,
+ gboolean several_chars,
+ gboolean cursor_pos_at_start,
+ gboolean cursor_pos_at_end,
+ gboolean start_is_inside_word,
+ gboolean start_ends_word,
+ gboolean end_is_inside_word,
+ gboolean end_ends_word)
+{
+ g_return_if_fail (GSPELL_IS_CURRENT_WORD_POLICY (policy));
+
+ if (!empty_selection ||
+ spans_several_lines ||
+ several_chars)
+ {
+ _gspell_current_word_policy_set_check_current_word (policy, TRUE);
+ }
+ /* Probably backspace key */
+ else if (cursor_pos_at_end)
+ {
+ if (start_is_inside_word || start_ends_word)
+ {
+ _gspell_current_word_policy_set_check_current_word (policy, FALSE);
+ }
+ else
+ {
+ _gspell_current_word_policy_set_check_current_word (policy, TRUE);
+ }
+ }
+ /* Probably delete key */
+ else if (cursor_pos_at_start)
+ {
+ if (end_is_inside_word || end_ends_word)
+ {
+ _gspell_current_word_policy_set_check_current_word (policy, FALSE);
+ }
+ else
+ {
+ _gspell_current_word_policy_set_check_current_word (policy, TRUE);
+ }
+ }
+ /* Text deleted programmatically */
+ else
+ {
+ _gspell_current_word_policy_set_check_current_word (policy, TRUE);
+ }
+}
diff --git a/gspell/gspell-current-word-policy.h b/gspell/gspell-current-word-policy.h
new file mode 100644
index 0000000..be3f69b
--- /dev/null
+++ b/gspell/gspell-current-word-policy.h
@@ -0,0 +1,87 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2016 - Sébastien Wilmet
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GSPELL_CURRENT_WORD_POLICY_H
+#define GSPELL_CURRENT_WORD_POLICY_H
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GSPELL_TYPE_CURRENT_WORD_POLICY (_gspell_current_word_policy_get_type ())
+
+G_GNUC_INTERNAL
+G_DECLARE_DERIVABLE_TYPE (GspellCurrentWordPolicy, _gspell_current_word_policy,
+ GSPELL, CURRENT_WORD_POLICY,
+ GObject)
+
+struct _GspellCurrentWordPolicyClass
+{
+ GObjectClass parent_class;
+};
+
+G_GNUC_INTERNAL
+GspellCurrentWordPolicy *
+ _gspell_current_word_policy_new (void);
+
+G_GNUC_INTERNAL
+gboolean _gspell_current_word_policy_get_check_current_word (GspellCurrentWordPolicy *policy);
+
+G_GNUC_INTERNAL
+void _gspell_current_word_policy_set_check_current_word (GspellCurrentWordPolicy *policy,
+ gboolean
check_current_word);
+
+G_GNUC_INTERNAL
+void _gspell_current_word_policy_session_cleared (GspellCurrentWordPolicy *policy);
+
+G_GNUC_INTERNAL
+void _gspell_current_word_policy_language_changed (GspellCurrentWordPolicy *policy);
+
+G_GNUC_INTERNAL
+void _gspell_current_word_policy_checker_changed (GspellCurrentWordPolicy *policy);
+
+G_GNUC_INTERNAL
+void _gspell_current_word_policy_cursor_moved (GspellCurrentWordPolicy *policy);
+
+G_GNUC_INTERNAL
+void _gspell_current_word_policy_several_chars_inserted (GspellCurrentWordPolicy *policy);
+
+G_GNUC_INTERNAL
+void _gspell_current_word_policy_single_char_inserted (GspellCurrentWordPolicy *policy,
+ gunichar ch,
+ gboolean
empty_selection,
+ gboolean
at_cursor_pos);
+
+G_GNUC_INTERNAL
+void _gspell_current_word_policy_text_deleted (GspellCurrentWordPolicy *policy,
+ gboolean
empty_selection,
+ gboolean
spans_several_lines,
+ gboolean
several_chars,
+ gboolean
cursor_pos_at_start,
+ gboolean
cursor_pos_at_end,
+ gboolean
start_is_inside_word,
+ gboolean
start_ends_word,
+ gboolean
end_is_inside_word,
+ gboolean
end_ends_word);
+
+G_END_DECLS
+
+#endif /* GSPELL_CURRENT_WORD_POLICY_H */
+
+/* ex:set ts=8 noet: */
diff --git a/gspell/gspell-entry-buffer.c b/gspell/gspell-entry-buffer.c
new file mode 100644
index 0000000..12401b4
--- /dev/null
+++ b/gspell/gspell-entry-buffer.c
@@ -0,0 +1,253 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2016 - Sébastien Wilmet
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "gspell-entry-buffer.h"
+
+/**
+ * SECTION:entry-buffer
+ * @Title: GspellEntryBuffer
+ * @Short_description: Spell checking support for GtkEntryBuffer
+ *
+ * #GspellEntryBuffer extends the #GtkEntryBuffer class with spell checking
+ * support.
+ */
+
+struct _GspellEntryBuffer
+{
+ GObject parent;
+
+ GtkEntryBuffer *buffer;
+ GspellChecker *spell_checker;
+};
+
+enum
+{
+ PROP_0,
+ PROP_BUFFER,
+ PROP_SPELL_CHECKER,
+};
+
+#define GSPELL_ENTRY_BUFFER_KEY "gspell-entry-buffer-key"
+
+G_DEFINE_TYPE (GspellEntryBuffer, gspell_entry_buffer, G_TYPE_OBJECT)
+
+static void
+gspell_entry_buffer_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GspellEntryBuffer *gspell_buffer = GSPELL_ENTRY_BUFFER (object);
+
+ switch (prop_id)
+ {
+ case PROP_BUFFER:
+ g_value_set_object (value, gspell_entry_buffer_get_buffer (gspell_buffer));
+ break;
+
+ case PROP_SPELL_CHECKER:
+ g_value_set_object (value, gspell_entry_buffer_get_spell_checker (gspell_buffer));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gspell_entry_buffer_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GspellEntryBuffer *gspell_buffer = GSPELL_ENTRY_BUFFER (object);
+
+ switch (prop_id)
+ {
+ case PROP_BUFFER:
+ g_assert (gspell_buffer->buffer == NULL);
+ gspell_buffer->buffer = g_value_get_object (value);
+ break;
+
+ case PROP_SPELL_CHECKER:
+ gspell_entry_buffer_set_spell_checker (gspell_buffer, g_value_get_object (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gspell_entry_buffer_dispose (GObject *object)
+{
+ GspellEntryBuffer *gspell_buffer = GSPELL_ENTRY_BUFFER (object);
+
+ gspell_buffer->buffer = NULL;
+ g_clear_object (&gspell_buffer->spell_checker);
+
+ G_OBJECT_CLASS (gspell_entry_buffer_parent_class)->dispose (object);
+}
+
+static void
+gspell_entry_buffer_class_init (GspellEntryBufferClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->get_property = gspell_entry_buffer_get_property;
+ object_class->set_property = gspell_entry_buffer_set_property;
+ object_class->dispose = gspell_entry_buffer_dispose;
+
+ /**
+ * GspellEntryBuffer:buffer:
+ *
+ * The #GtkEntryBuffer.
+ *
+ * Since: 1.4
+ */
+ g_object_class_install_property (object_class,
+ PROP_BUFFER,
+ g_param_spec_object ("buffer",
+ "Buffer",
+ "",
+ GTK_TYPE_ENTRY_BUFFER,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * GspellEntryBuffer:spell-checker:
+ *
+ * The #GspellChecker.
+ *
+ * Since: 1.4
+ */
+ g_object_class_install_property (object_class,
+ PROP_SPELL_CHECKER,
+ g_param_spec_object ("spell-checker",
+ "Spell Checker",
+ "",
+ GSPELL_TYPE_CHECKER,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+}
+
+static void
+gspell_entry_buffer_init (GspellEntryBuffer *gspell_buffer)
+{
+}
+
+/**
+ * gspell_entry_buffer_get_from_gtk_entry_buffer:
+ * @gtk_buffer: a #GtkEntryBuffer.
+ *
+ * Returns the #GspellEntryBuffer of @gtk_buffer. The returned object is
+ * guaranteed to be the same for the lifetime of @gtk_buffer.
+ *
+ * Returns: (transfer none): the #GspellEntryBuffer of @gtk_buffer.
+ * Since: 1.4
+ */
+GspellEntryBuffer *
+gspell_entry_buffer_get_from_gtk_entry_buffer (GtkEntryBuffer *gtk_buffer)
+{
+ GspellEntryBuffer *gspell_buffer;
+
+ g_return_val_if_fail (GTK_IS_ENTRY_BUFFER (gtk_buffer), NULL);
+
+ gspell_buffer = g_object_get_data (G_OBJECT (gtk_buffer), GSPELL_ENTRY_BUFFER_KEY);
+
+ if (gspell_buffer == NULL)
+ {
+ gspell_buffer = g_object_new (GSPELL_TYPE_ENTRY_BUFFER,
+ "buffer", gtk_buffer,
+ NULL);
+
+ g_object_set_data_full (G_OBJECT (gtk_buffer),
+ GSPELL_ENTRY_BUFFER_KEY,
+ gspell_buffer,
+ g_object_unref);
+ }
+
+ g_return_val_if_fail (GSPELL_IS_ENTRY_BUFFER (gspell_buffer), NULL);
+ return gspell_buffer;
+}
+
+/**
+ * gspell_entry_buffer_get_buffer:
+ * @gspell_buffer: a #GspellEntryBuffer.
+ *
+ * Returns: (transfer none): the #GtkEntryBuffer of @gspell_buffer.
+ * Since: 1.4
+ */
+GtkEntryBuffer *
+gspell_entry_buffer_get_buffer (GspellEntryBuffer *gspell_buffer)
+{
+ g_return_val_if_fail (GSPELL_IS_ENTRY_BUFFER (gspell_buffer), NULL);
+
+ return gspell_buffer->buffer;
+}
+
+/**
+ * gspell_entry_buffer_get_spell_checker:
+ * @gspell_buffer: a #GspellEntryBuffer.
+ *
+ * Returns: (nullable) (transfer none): the #GspellChecker if one has been set,
+ * or %NULL.
+ * Since: 1.4
+ */
+GspellChecker *
+gspell_entry_buffer_get_spell_checker (GspellEntryBuffer *gspell_buffer)
+{
+ g_return_val_if_fail (GSPELL_IS_ENTRY_BUFFER (gspell_buffer), NULL);
+
+ return gspell_buffer->spell_checker;
+}
+
+/**
+ * gspell_entry_buffer_set_spell_checker:
+ * @gspell_buffer: a #GspellEntryBuffer.
+ * @spell_checker: (nullable): a #GspellChecker, or %NULL to unset the spell
+ * checker.
+ *
+ * Sets a #GspellChecker to a #GspellEntryBuffer. The @gspell_buffer will own a
+ * reference to @spell_checker, so you can release your reference to
+ * @spell_checker if you no longer need it.
+ *
+ * Since: 1.4
+ */
+void
+gspell_entry_buffer_set_spell_checker (GspellEntryBuffer *gspell_buffer,
+ GspellChecker *spell_checker)
+{
+ g_return_if_fail (GSPELL_IS_ENTRY_BUFFER (gspell_buffer));
+ g_return_if_fail (spell_checker == NULL || GSPELL_IS_CHECKER (spell_checker));
+
+ if (g_set_object (&gspell_buffer->spell_checker, spell_checker))
+ {
+ g_object_notify (G_OBJECT (gspell_buffer), "spell-checker");
+ }
+}
+
+/* ex:set ts=8 noet: */
diff --git a/gspell/gspell-entry-buffer.h b/gspell/gspell-entry-buffer.h
new file mode 100644
index 0000000..73d85fd
--- /dev/null
+++ b/gspell/gspell-entry-buffer.h
@@ -0,0 +1,57 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2016 - Sébastien Wilmet
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GSPELL_ENTRY_BUFFER_H
+#define GSPELL_ENTRY_BUFFER_H
+
+#if !defined (GSPELL_H_INSIDE) && !defined (GSPELL_COMPILATION)
+#error "Only <gspell/gspell.h> can be included directly."
+#endif
+
+#include <gspell/gspell-checker.h>
+#include <gspell/gspell-version.h>
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GSPELL_TYPE_ENTRY_BUFFER (gspell_entry_buffer_get_type ())
+
+GSPELL_AVAILABLE_IN_1_4
+G_DECLARE_FINAL_TYPE (GspellEntryBuffer, gspell_entry_buffer,
+ GSPELL, ENTRY_BUFFER,
+ GObject)
+
+GSPELL_AVAILABLE_IN_1_4
+GspellEntryBuffer * gspell_entry_buffer_get_from_gtk_entry_buffer (GtkEntryBuffer *gtk_buffer);
+
+GSPELL_AVAILABLE_IN_1_4
+GtkEntryBuffer * gspell_entry_buffer_get_buffer (GspellEntryBuffer *gspell_buffer);
+
+GSPELL_AVAILABLE_IN_1_4
+GspellChecker * gspell_entry_buffer_get_spell_checker (GspellEntryBuffer
*gspell_buffer);
+
+GSPELL_AVAILABLE_IN_1_4
+void gspell_entry_buffer_set_spell_checker (GspellEntryBuffer *gspell_buffer,
+ GspellChecker *spell_checker);
+
+G_END_DECLS
+
+#endif /* GSPELL_ENTRY_BUFFER_H */
+
+/* ex:set ts=8 noet: */
diff --git a/gspell/gspell-entry-private.h b/gspell/gspell-entry-private.h
new file mode 100644
index 0000000..8e5c050
--- /dev/null
+++ b/gspell/gspell-entry-private.h
@@ -0,0 +1,34 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2016 - Sébastien Wilmet
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GSPELL_ENTRY_PRIVATE_H
+#define GSPELL_ENTRY_PRIVATE_H
+
+#include "gspell/gspell-entry.h"
+
+G_BEGIN_DECLS
+
+G_GNUC_INTERNAL
+const GSList * _gspell_entry_get_misspelled_words (GspellEntry *gspell_entry);
+
+G_END_DECLS
+
+#endif /* GSPELL_ENTRY_PRIVATE_H */
+
+/* ex:set ts=8 noet: */
diff --git a/gspell/gspell-entry-utils.c b/gspell/gspell-entry-utils.c
new file mode 100644
index 0000000..a2c9da1
--- /dev/null
+++ b/gspell/gspell-entry-utils.c
@@ -0,0 +1,252 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2016 - Sébastien Wilmet
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "gspell-entry-utils.h"
+#include <string.h>
+#include "gspell-utils.h"
+
+GspellEntryWord *
+_gspell_entry_word_new (void)
+{
+ return g_new0 (GspellEntryWord, 1);
+}
+
+void
+_gspell_entry_word_free (gpointer data)
+{
+ GspellEntryWord *word = data;
+
+ if (word != NULL)
+ {
+ g_free (word->word_str);
+ g_free (word);
+ }
+}
+
+/* Without the preedit string.
+ * Free @log_attrs with g_free().
+ */
+static void
+get_pango_log_attrs (GtkEntry *entry,
+ PangoLogAttr **log_attrs,
+ gint *n_attrs)
+{
+ GtkEntryBuffer *buffer;
+ const gchar *text;
+
+ g_assert (log_attrs != NULL);
+ g_assert (n_attrs != NULL);
+
+ buffer = gtk_entry_get_buffer (entry);
+ text = gtk_entry_buffer_get_text (buffer);
+
+ *n_attrs = gtk_entry_buffer_get_length (buffer) + 1;
+ *log_attrs = g_new0 (PangoLogAttr, *n_attrs);
+
+ pango_get_log_attrs (text,
+ gtk_entry_buffer_get_bytes (buffer),
+ -1,
+ NULL,
+ *log_attrs,
+ *n_attrs);
+
+ _gspell_utils_improve_word_boundaries (text, *log_attrs, *n_attrs);
+}
+
+/* Returns: (transfer full) (element-type GspellEntryWord): the list of words in
+ * @entry, without the preedit string. Free with
+ * g_slist_free_full (words, _gspell_entry_word_free);
+ *
+ * The preedit string is not included, because the current word being typed
+ * should not be marked as misspelled, so it doesn't change whether the preedit
+ * string is included or not, and the code is simpler without.
+ */
+GSList *
+_gspell_entry_utils_get_words (GtkEntry *entry)
+{
+ const gchar *text;
+ const gchar *cur_text_pos;
+ const gchar *word_start;
+ gint word_start_char_pos;
+ PangoLogAttr *attrs;
+ gint n_attrs;
+ gint attr_num;
+ GSList *list = NULL;
+
+ g_return_val_if_fail (GTK_IS_ENTRY (entry), NULL);
+
+ text = gtk_entry_get_text (entry);
+
+ if (text == NULL || text[0] == '\0')
+ {
+ return NULL;
+ }
+
+ get_pango_log_attrs (entry, &attrs, &n_attrs);
+
+ attr_num = 0;
+ cur_text_pos = text;
+ word_start = NULL;
+ word_start_char_pos = 0;
+
+ while (attr_num < n_attrs)
+ {
+ if (word_start != NULL &&
+ attrs[attr_num].is_word_end)
+ {
+ const gchar *word_end;
+ GspellEntryWord *word;
+
+ if (cur_text_pos != NULL)
+ {
+ word_end = cur_text_pos;
+ }
+ else
+ {
+ word_end = word_start + strlen (word_start);
+ }
+
+ word = _gspell_entry_word_new ();
+ word->byte_start = word_start - text;
+ word->byte_end = word_end - text;
+ word->char_start = word_start_char_pos;
+ word->char_end = attr_num;
+ word->word_str = g_strndup (word_start, word_end - word_start);
+
+ list = g_slist_prepend (list, word);
+
+ /* Find next word start. */
+ word_start = NULL;
+ }
+
+ if (word_start == NULL &&
+ attrs[attr_num].is_word_start)
+ {
+ word_start = cur_text_pos;
+ word_start_char_pos = attr_num;
+ }
+
+ if (attr_num == n_attrs - 1 ||
+ cur_text_pos == NULL ||
+ cur_text_pos[0] == '\0')
+ {
+ break;
+ }
+
+ attr_num++;
+ cur_text_pos = g_utf8_find_next_char (cur_text_pos, NULL);
+ }
+
+ /* Sanity checks */
+
+ if (attr_num != n_attrs - 1)
+ {
+ g_warning ("%s(): problem in loop iteration, attr_num=%d but should be %d. "
+ "End of string reached too early.",
+ G_STRFUNC,
+ attr_num,
+ n_attrs - 1);
+ }
+
+ if (cur_text_pos != NULL && cur_text_pos[0] != '\0')
+ {
+ g_warning ("%s(): end of string not reached.", G_STRFUNC);
+ }
+
+ g_free (attrs);
+ return g_slist_reverse (list);
+}
+
+static gint
+get_layout_index (GtkEntry *entry,
+ gint x)
+{
+ PangoLayout *layout;
+ PangoLayoutLine *line;
+ gint layout_index; /* in bytes */
+ gint trailing_chars;
+ const gchar *layout_text;
+ const gchar *pos_in_layout_text;
+ gint layout_text_byte_length;
+ gint max_trailing_chars;
+
+ layout = gtk_entry_get_layout (entry);
+ line = pango_layout_get_line_readonly (layout, 0);
+
+ pango_layout_line_x_to_index (line,
+ x * PANGO_SCALE,
+ &layout_index,
+ &trailing_chars);
+
+ layout_text = pango_layout_get_text (layout);
+
+ /* Performance should not be a problem here, it's better too much
+ * security than too few.
+ */
+ layout_text_byte_length = strlen (layout_text);
+ if (layout_index >= layout_text_byte_length)
+ {
+ return layout_text_byte_length;
+ }
+
+ if (trailing_chars == 0)
+ {
+ return layout_index;
+ }
+
+ pos_in_layout_text = layout_text + layout_index;
+ max_trailing_chars = g_utf8_strlen (pos_in_layout_text, -1);
+ trailing_chars = MIN (trailing_chars, max_trailing_chars);
+
+ pos_in_layout_text = g_utf8_offset_to_pointer (pos_in_layout_text, trailing_chars);
+
+ return pos_in_layout_text - layout_text;
+}
+
+/* The return value is in characters, not bytes. And a position suitable for the
+ * text in the GtkEntryBuffer, i.e. without the preedit string.
+ */
+gint
+_gspell_entry_utils_get_char_position_at_event (GtkEntry *entry,
+ GdkEventButton *event)
+{
+ gint scroll_offset;
+ gint x;
+ gint layout_index; /* in bytes */
+ gint text_index; /* in bytes */
+ const gchar *buffer_text;
+
+ g_object_get (entry,
+ "scroll-offset", &scroll_offset,
+ NULL);
+
+ x = event->x + scroll_offset;
+
+ layout_index = get_layout_index (entry, x);
+ text_index = gtk_entry_layout_index_to_text_index (entry, layout_index);
+
+ buffer_text = gtk_entry_get_text (entry);
+ return g_utf8_pointer_to_offset (buffer_text, buffer_text + text_index);
+}
+
+/* ex:set ts=8 noet: */
diff --git a/gspell/gspell-entry-utils.h b/gspell/gspell-entry-utils.h
new file mode 100644
index 0000000..7370f81
--- /dev/null
+++ b/gspell/gspell-entry-utils.h
@@ -0,0 +1,62 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2016 - Sébastien Wilmet
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GSPELL_ENTRY_UTILS_H
+#define GSPELL_ENTRY_UTILS_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+typedef struct _GspellEntryWord GspellEntryWord;
+struct _GspellEntryWord
+{
+ gchar *word_str;
+
+ /* Position in the GtkEntryBuffer. The character at the byte_end index
+ * is not included, like in #PangoAttribute.
+ */
+ gint byte_start;
+ gint byte_end;
+
+ /* The same as @byte_start and @byte_end, but in characters.
+ * Useful for example for the #GtkEditable functions.
+ */
+ gint char_start;
+ gint char_end;
+};
+
+G_GNUC_INTERNAL
+GspellEntryWord *_gspell_entry_word_new (void);
+
+G_GNUC_INTERNAL
+void _gspell_entry_word_free (gpointer data);
+
+G_GNUC_INTERNAL
+GSList * _gspell_entry_utils_get_words (GtkEntry *entry);
+
+G_GNUC_INTERNAL
+gint _gspell_entry_utils_get_char_position_at_event (GtkEntry *entry,
+ GdkEventButton *event);
+
+G_END_DECLS
+
+#endif /* GSPELL_ENTRY_UTILS_H */
+
+/* ex:set ts=8 noet: */
diff --git a/gspell/gspell-entry.c b/gspell/gspell-entry.c
new file mode 100644
index 0000000..5dbec49
--- /dev/null
+++ b/gspell/gspell-entry.c
@@ -0,0 +1,1234 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2016, 2017 - Sébastien Wilmet
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "gspell-entry.h"
+#include "gspell-entry-private.h"
+#include "gspell-entry-buffer.h"
+#include "gspell-entry-utils.h"
+#include "gspell-context-menu.h"
+#include "gspell-current-word-policy.h"
+#include "gspell-utils.h"
+
+/**
+ * SECTION:entry
+ * @Title: GspellEntry
+ * @Short_description: Spell checking support for GtkEntry
+ *
+ * #GspellEntry extends the #GtkEntry class with inline spell checking.
+ * Misspelled words are highlighted with a red %PANGO_UNDERLINE_SINGLE.
+ * Right-clicking a misspelled word pops up a context menu of suggested
+ * replacements. The context menu also contains an “Ignore All” item to add the
+ * misspelled word to the session dictionary. And an “Add” item to add the word
+ * to the personal dictionary.
+ *
+ * For a basic use-case, there is the gspell_entry_basic_setup() convenience
+ * function.
+ *
+ * If you don't use the gspell_entry_basic_setup() function, you need to call
+ * gspell_entry_buffer_set_spell_checker() to associate a #GspellChecker to the
+ * #GtkEntryBuffer.
+ *
+ * Note that #GspellEntry extends the #GtkEntry class but without subclassing
+ * it, because #GtkEntry is already subclassed by #GtkSearchEntry for example.
+ *
+ * %PANGO_UNDERLINE_SINGLE is used for consistency with #GspellTextView.
+ * If you want a %PANGO_UNDERLINE_ERROR instead (a wavy underline), please fix
+ * [this bug](https://bugzilla.gnome.org/show_bug.cgi?id=763741) first.
+ */
+
+struct _GspellEntry
+{
+ GObject parent;
+
+ GtkEntry *entry;
+ GtkEntryBuffer *buffer;
+ GspellChecker *checker;
+
+ GspellCurrentWordPolicy *current_word_policy;
+
+ /* List elements: GspellEntryWord*.
+ * Used for unit tests.
+ */
+ GSList *misspelled_words;
+
+ /* The position is in characters, not in bytes. */
+ gint popup_char_position;
+
+ gulong notify_attributes_handler_id;
+ guint notify_attributes_idle_id;
+
+ guint inline_spell_checking : 1;
+};
+
+enum
+{
+ PROP_0,
+ PROP_ENTRY,
+ PROP_INLINE_SPELL_CHECKING,
+};
+
+#define GSPELL_ENTRY_KEY "gspell-entry-key"
+
+G_DEFINE_TYPE (GspellEntry, gspell_entry, G_TYPE_OBJECT)
+
+/* This function should be called instead of accessing the inline_spell_checking
+ * attribute.
+ */
+static gboolean
+inline_spell_checking_is_enabled (GspellEntry *gspell_entry)
+{
+ /* The GtkEntry:input-purpose and/or GtkEntry:input-hints could be taken
+ * into account here, but it is not the case. There is already the
+ * GspellEntry:inline-spell-checking property, which needs to be FALSE
+ * by default. If it was TRUE by default, an application would just need
+ * to call gspell_entry_get_from_gtk_entry(), but it would be strange to
+ * do nothing with the returned GspellEntry. So inline-spell-checking is
+ * FALSE by default and the application anyway needs to set it to TRUE
+ * manually to enable the *inline* spell checking (a GtkEntry could have
+ * other types of spell checking, for example based on GspellNavigator
+ * to check an entire form or check a list of forms, even though such
+ * feature is probably rare).
+ *
+ * In other words, it might be desirable to set
+ * GTK_INPUT_HINT_SPELLCHECK but keeping the inline spell checking of
+ * GspellEntry disabled. But when the inline spell checker of
+ * GspellEntry is enabled, it is normally always desirable to set
+ * GTK_INPUT_HINT_SPELLCHECK, which can be seen as duplicated state, but
+ * it is not, because if the GspellEntry:inline-spell-checking property
+ * is removed, another boolean property would be needed to tell
+ * GspellEntry whether it needs to bind the input-hints settings to its
+ * inline spell checker.
+ *
+ * Anyway, the mere fact of calling gspell_entry_get_from_gtk_entry()
+ * should not have unexpected side effects.
+ */
+
+ return (gspell_entry->inline_spell_checking &&
+ gtk_entry_get_visibility (gspell_entry->entry));
+}
+
+static void
+set_attributes (GspellEntry *gspell_entry,
+ PangoAttrList *attributes)
+{
+ g_signal_handler_block (gspell_entry->entry,
+ gspell_entry->notify_attributes_handler_id);
+
+ gtk_entry_set_attributes (gspell_entry->entry, attributes);
+
+ g_signal_handler_unblock (gspell_entry->entry,
+ gspell_entry->notify_attributes_handler_id);
+}
+
+static void
+update_attributes (GspellEntry *gspell_entry)
+{
+ PangoAttrList *attr_list;
+
+ /* If attributes have been added or removed from an existing
+ * PangoAttrList, GtkEntry doesn't know that the :attributes property
+ * has been modified. Without this code, GtkEntry can become buggy,
+ * especially with multi-byte characters (displaying them as unknown
+ * char boxes).
+ */
+ attr_list = gtk_entry_get_attributes (gspell_entry->entry);
+ set_attributes (gspell_entry, attr_list);
+}
+
+static gboolean
+remove_underlines_filter (PangoAttribute *attr,
+ gpointer user_data)
+{
+ return (attr->klass->type == PANGO_ATTR_UNDERLINE ||
+ attr->klass->type == PANGO_ATTR_UNDERLINE_COLOR);
+}
+
+static void
+remove_all_underlines (GspellEntry *gspell_entry)
+{
+ PangoAttrList *attr_list;
+
+ attr_list = gtk_entry_get_attributes (gspell_entry->entry);
+
+ if (attr_list == NULL)
+ {
+ return;
+ }
+
+ pango_attr_list_filter (attr_list,
+ remove_underlines_filter,
+ NULL);
+
+ update_attributes (gspell_entry);
+}
+
+static void
+insert_underline (GspellEntry *gspell_entry,
+ guint byte_start,
+ guint byte_end)
+{
+ PangoAttribute *attr_underline;
+ PangoAttribute *attr_underline_color;
+ PangoAttrList *attr_list;
+
+ attr_underline = pango_attr_underline_new (PANGO_UNDERLINE_SINGLE);
+ attr_underline->start_index = byte_start;
+ attr_underline->end_index = byte_end;
+
+ attr_underline_color = _gspell_utils_create_pango_attr_underline_color ();
+ attr_underline_color->start_index = byte_start;
+ attr_underline_color->end_index = byte_end;
+
+ attr_list = gtk_entry_get_attributes (gspell_entry->entry);
+
+ if (attr_list == NULL)
+ {
+ attr_list = pango_attr_list_new ();
+ set_attributes (gspell_entry, attr_list);
+ pango_attr_list_unref (attr_list);
+ }
+
+ /* Do not use pango_attr_list_change(), because all previous underlines
+ * are anyway removed by remove_all_underlines().
+ */
+ pango_attr_list_insert (attr_list, attr_underline);
+ pango_attr_list_insert (attr_list, attr_underline_color);
+}
+
+static void
+update_misspelled_words_list (GspellEntry *gspell_entry)
+{
+ GSList *all_words;
+
+ g_slist_free_full (gspell_entry->misspelled_words, _gspell_entry_word_free);
+ gspell_entry->misspelled_words = NULL;
+
+ if (!inline_spell_checking_is_enabled (gspell_entry))
+ {
+ return;
+ }
+
+ if (gspell_entry->checker == NULL ||
+ gspell_checker_get_language (gspell_entry->checker) == NULL)
+ {
+ return;
+ }
+
+ all_words = _gspell_entry_utils_get_words (gspell_entry->entry);
+
+ while (all_words != NULL)
+ {
+ GspellEntryWord *cur_word = all_words->data;
+ gboolean correctly_spelled;
+ GError *error = NULL;
+
+ correctly_spelled = gspell_checker_check_word (gspell_entry->checker,
+ cur_word->word_str, -1,
+ &error);
+
+ if (error != NULL)
+ {
+ g_warning ("Inline spell checker: %s", error->message);
+ g_clear_error (&error);
+ g_slist_free_full (all_words, _gspell_entry_word_free);
+ all_words = NULL;
+ break;
+ }
+
+ if (correctly_spelled)
+ {
+ _gspell_entry_word_free (cur_word);
+ }
+ else
+ {
+ gspell_entry->misspelled_words = g_slist_prepend (gspell_entry->misspelled_words,
+ cur_word);
+ }
+
+ all_words = g_slist_delete_link (all_words, all_words);
+ }
+
+ g_assert (all_words == NULL);
+
+ gspell_entry->misspelled_words = g_slist_reverse (gspell_entry->misspelled_words);
+}
+
+static gboolean
+is_current_word (GspellEntry *gspell_entry,
+ GspellEntryWord *word)
+{
+ gint cursor_pos;
+
+ cursor_pos = gtk_editable_get_position (GTK_EDITABLE (gspell_entry->entry));
+
+ return (word->char_start <= cursor_pos && cursor_pos <= word->char_end);
+}
+
+/* If another feature wants to insert underlines in another color (e.g. for
+ * grammar checking), this won't work well. A previous implementation used the
+ * GtkEditable::changed signal: removing all underlines in the second emission
+ * stage, and inserting new underlines in the fourth emission stage. That way
+ * another feature could connect to the ::changed signal and insert other
+ * underlines. But it broke the semantics of the ::changed signal, since it was
+ * emitted a lot of times without changes in the content.
+ *
+ * So, if one day someone wants to implement another feature that inserts other
+ * underlines, a new GtkEntry API would be needed to have a clean solution,
+ * instead of stepping on other's feet. For example GtkTextView has a
+ * higher-level API to insert tags, set priorities on them, etc.
+ */
+static void
+recheck_all (GspellEntry *gspell_entry)
+{
+ GSList *l;
+
+ remove_all_underlines (gspell_entry);
+
+ update_misspelled_words_list (gspell_entry);
+
+ for (l = gspell_entry->misspelled_words; l != NULL; l = l->next)
+ {
+ GspellEntryWord *cur_word = l->data;
+
+ if (!_gspell_current_word_policy_get_check_current_word (gspell_entry->current_word_policy) &&
+ is_current_word (gspell_entry, cur_word))
+ {
+ continue;
+ }
+
+ insert_underline (gspell_entry,
+ cur_word->byte_start,
+ cur_word->byte_end);
+ }
+
+ update_attributes (gspell_entry);
+}
+
+static void
+changed_after_cb (GtkEditable *editable,
+ GspellEntry *gspell_entry)
+{
+ recheck_all (gspell_entry);
+}
+
+static gboolean
+notify_attributes_idle_cb (gpointer user_data)
+{
+ GspellEntry *gspell_entry = GSPELL_ENTRY (user_data);
+
+ /* Re-apply our attributes. Do it in an idle function, to not be inside
+ * a notify::attributes signal emission. If we call recheck_all() during
+ * the signal emission, there is an infinite loop.
+ */
+ recheck_all (gspell_entry);
+
+ gspell_entry->notify_attributes_idle_id = 0;
+ return G_SOURCE_REMOVE;
+}
+
+static void
+notify_attributes_cb (GtkEntry *gtk_entry,
+ GParamSpec *pspec,
+ GspellEntry *gspell_entry)
+{
+ if (gspell_entry->notify_attributes_idle_id == 0)
+ {
+ gspell_entry->notify_attributes_idle_id =
+ g_idle_add_full (G_PRIORITY_HIGH_IDLE,
+ notify_attributes_idle_cb,
+ gspell_entry,
+ NULL);
+ }
+}
+
+static void
+language_notify_cb (GspellChecker *checker,
+ GParamSpec *pspec,
+ GspellEntry *gspell_entry)
+{
+ _gspell_current_word_policy_language_changed (gspell_entry->current_word_policy);
+ recheck_all (gspell_entry);
+}
+
+static void
+session_cleared_cb (GspellChecker *checker,
+ GspellEntry *gspell_entry)
+{
+ _gspell_current_word_policy_session_cleared (gspell_entry->current_word_policy);
+ recheck_all (gspell_entry);
+}
+
+static void
+set_checker (GspellEntry *gspell_entry,
+ GspellChecker *checker)
+{
+ if (gspell_entry->checker == checker)
+ {
+ return;
+ }
+
+ if (gspell_entry->checker != NULL)
+ {
+ g_signal_handlers_disconnect_by_func (gspell_entry->checker,
+ language_notify_cb,
+ gspell_entry);
+
+ g_signal_handlers_disconnect_by_func (gspell_entry->checker,
+ session_cleared_cb,
+ gspell_entry);
+
+ g_signal_handlers_disconnect_by_func (gspell_entry->checker,
+ recheck_all,
+ gspell_entry);
+
+ g_object_unref (gspell_entry->checker);
+ }
+
+ gspell_entry->checker = checker;
+
+ if (gspell_entry->checker != NULL)
+ {
+ g_signal_connect (gspell_entry->checker,
+ "notify::language",
+ G_CALLBACK (language_notify_cb),
+ gspell_entry);
+
+ g_signal_connect (gspell_entry->checker,
+ "session-cleared",
+ G_CALLBACK (session_cleared_cb),
+ gspell_entry);
+
+ g_signal_connect_swapped (gspell_entry->checker,
+ "word-added-to-personal",
+ G_CALLBACK (recheck_all),
+ gspell_entry);
+
+ g_signal_connect_swapped (gspell_entry->checker,
+ "word-added-to-session",
+ G_CALLBACK (recheck_all),
+ gspell_entry);
+
+ g_object_ref (gspell_entry->checker);
+ }
+}
+
+static void
+update_checker (GspellEntry *gspell_entry)
+{
+ GspellChecker *checker = NULL;
+
+ if (gspell_entry->buffer != NULL)
+ {
+ GspellEntryBuffer *gspell_buffer;
+
+ gspell_buffer = gspell_entry_buffer_get_from_gtk_entry_buffer (gspell_entry->buffer);
+ checker = gspell_entry_buffer_get_spell_checker (gspell_buffer);
+ }
+
+ set_checker (gspell_entry, checker);
+}
+
+static void
+notify_spell_checker_cb (GspellEntryBuffer *gspell_buffer,
+ GParamSpec *pspec,
+ GspellEntry *gspell_entry)
+{
+ update_checker (gspell_entry);
+
+ _gspell_current_word_policy_checker_changed (gspell_entry->current_word_policy);
+ recheck_all (gspell_entry);
+}
+
+static void
+inserted_text_cb (GtkEntryBuffer *buffer,
+ guint position,
+ gchar *chars,
+ guint n_chars,
+ GspellEntry *gspell_entry)
+{
+ if (n_chars > 1)
+ {
+ _gspell_current_word_policy_several_chars_inserted (gspell_entry->current_word_policy);
+ }
+ else
+ {
+ gunichar ch;
+ gboolean empty_selection;
+ gint cursor_pos;
+ gboolean at_cursor_pos;
+
+ ch = g_utf8_get_char (chars);
+
+ empty_selection = !gtk_editable_get_selection_bounds (GTK_EDITABLE (gspell_entry->entry),
+ NULL,
+ NULL);
+
+ cursor_pos = gtk_editable_get_position (GTK_EDITABLE (gspell_entry->entry));
+ at_cursor_pos = cursor_pos == (gint)position;
+
+ _gspell_current_word_policy_single_char_inserted (gspell_entry->current_word_policy,
+ ch,
+ empty_selection,
+ at_cursor_pos);
+ }
+}
+
+static void
+set_buffer (GspellEntry *gspell_entry,
+ GtkEntryBuffer *gtk_buffer)
+{
+ GspellEntryBuffer *gspell_buffer;
+
+ if (gspell_entry->buffer == gtk_buffer)
+ {
+ return;
+ }
+
+ if (gspell_entry->buffer != NULL)
+ {
+ gspell_buffer = gspell_entry_buffer_get_from_gtk_entry_buffer (gspell_entry->buffer);
+
+ g_signal_handlers_disconnect_by_func (gspell_buffer,
+ notify_spell_checker_cb,
+ gspell_entry);
+
+ g_signal_handlers_disconnect_by_func (gspell_entry->buffer,
+ inserted_text_cb,
+ gspell_entry);
+
+ g_object_unref (gspell_entry->buffer);
+ }
+
+ gspell_entry->buffer = gtk_buffer;
+
+ if (gspell_entry->buffer != NULL)
+ {
+ gspell_buffer = gspell_entry_buffer_get_from_gtk_entry_buffer (gspell_entry->buffer);
+
+ g_signal_connect (gspell_buffer,
+ "notify::spell-checker",
+ G_CALLBACK (notify_spell_checker_cb),
+ gspell_entry);
+
+ g_signal_connect (gspell_entry->buffer,
+ "inserted-text",
+ G_CALLBACK (inserted_text_cb),
+ gspell_entry);
+
+ g_object_ref (gspell_entry->buffer);
+ }
+
+ update_checker (gspell_entry);
+}
+
+static void
+update_buffer (GspellEntry *gspell_entry)
+{
+ set_buffer (gspell_entry, gtk_entry_get_buffer (gspell_entry->entry));
+}
+
+static void
+notify_buffer_cb (GtkEntry *gtk_entry,
+ GParamSpec *pspec,
+ GspellEntry *gspell_entry)
+{
+ update_buffer (gspell_entry);
+ recheck_all (gspell_entry);
+}
+
+/* Free the return value with _gspell_entry_word_free(). */
+static GspellEntryWord *
+get_entry_word_at_popup_position (GspellEntry *gspell_entry)
+{
+ gint pos;
+ GSList *words;
+ GSList *l;
+ GspellEntryWord *entry_word = NULL;
+
+ pos = gspell_entry->popup_char_position;
+
+ words = _gspell_entry_utils_get_words (gspell_entry->entry);
+
+ for (l = words; l != NULL; l = l->next)
+ {
+ GspellEntryWord *cur_word = l->data;
+
+ if (cur_word->char_start <= pos && pos <= cur_word->char_end)
+ {
+ entry_word = cur_word;
+ l->data = NULL;
+ break;
+ }
+ }
+
+ g_slist_free_full (words, _gspell_entry_word_free);
+ return entry_word;
+}
+
+static gboolean
+popup_menu_cb (GtkEntry *gtk_entry,
+ GspellEntry *gspell_entry)
+{
+ /* Save the position before popping up the menu, otherwise it will
+ * contain the wrong set of suggestions.
+ */
+ gspell_entry->popup_char_position = gtk_editable_get_position (GTK_EDITABLE (gtk_entry));
+
+ return FALSE;
+}
+
+static gboolean
+button_press_event_cb (GtkEntry *gtk_entry,
+ GdkEventButton *event,
+ GspellEntry *gspell_entry)
+{
+ if (event->button == GDK_BUTTON_SECONDARY)
+ {
+ gspell_entry->popup_char_position =
+ _gspell_entry_utils_get_char_position_at_event (gtk_entry, event);
+ }
+
+ _gspell_current_word_policy_cursor_moved (gspell_entry->current_word_policy);
+ recheck_all (gspell_entry);
+
+ return GDK_EVENT_PROPAGATE;
+}
+
+static void
+language_activated_cb (const GspellLanguage *lang,
+ gpointer user_data)
+{
+ GspellEntry *gspell_entry;
+
+ g_return_if_fail (GSPELL_IS_ENTRY (user_data));
+
+ gspell_entry = GSPELL_ENTRY (user_data);
+
+ if (gspell_entry->checker != NULL)
+ {
+ gspell_checker_set_language (gspell_entry->checker, lang);
+ }
+}
+
+static void
+suggestion_activated_cb (const gchar *suggested_word,
+ gpointer user_data)
+{
+ GspellEntry *gspell_entry;
+ GspellEntryWord *word;
+ gint pos;
+
+ g_return_if_fail (GSPELL_IS_ENTRY (user_data));
+
+ gspell_entry = GSPELL_ENTRY (user_data);
+
+ word = get_entry_word_at_popup_position (gspell_entry);
+ if (word == NULL)
+ {
+ return;
+ }
+
+ gtk_editable_delete_text (GTK_EDITABLE (gspell_entry->entry),
+ word->char_start,
+ word->char_end);
+
+ pos = word->char_start;
+ gtk_editable_insert_text (GTK_EDITABLE (gspell_entry->entry),
+ suggested_word, -1,
+ &pos);
+
+ _gspell_entry_word_free (word);
+}
+
+static void
+populate_popup_cb (GtkEntry *gtk_entry,
+ GtkWidget *popup,
+ GspellEntry *gspell_entry)
+{
+ GtkMenu *menu;
+ GtkWidget *menu_item;
+ GtkMenuItem *lang_menu_item;
+ GtkMenuItem *suggestions_menu_item;
+ const GspellLanguage *current_language;
+ GspellEntryWord *word;
+ gboolean correctly_spelled;
+ GError *error = NULL;
+
+ if (!GTK_IS_MENU (popup))
+ {
+ return;
+ }
+
+ menu = GTK_MENU (popup);
+
+ if (!inline_spell_checking_is_enabled (gspell_entry))
+ {
+ return;
+ }
+
+ if (gspell_entry->checker == NULL)
+ {
+ return;
+ }
+
+ /* Prepend separator */
+ menu_item = gtk_separator_menu_item_new ();
+ gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), menu_item);
+ gtk_widget_show (menu_item);
+
+ /* Prepend language sub-menu */
+ current_language = gspell_checker_get_language (gspell_entry->checker);
+ lang_menu_item = _gspell_context_menu_get_language_menu_item (current_language,
+ language_activated_cb,
+ gspell_entry);
+
+ gtk_menu_shell_prepend (GTK_MENU_SHELL (menu),
+ GTK_WIDGET (lang_menu_item));
+
+ /* Prepend suggestions sub-menu */
+ word = get_entry_word_at_popup_position (gspell_entry);
+
+ if (word == NULL)
+ {
+ return;
+ }
+
+ correctly_spelled = gspell_checker_check_word (gspell_entry->checker,
+ word->word_str, -1,
+ &error);
+
+ if (error != NULL)
+ {
+ g_warning ("Inline spell checker: %s", error->message);
+ g_clear_error (&error);
+ _gspell_entry_word_free (word);
+ return;
+ }
+
+ if (!correctly_spelled)
+ {
+ suggestions_menu_item = _gspell_context_menu_get_suggestions_menu_item (gspell_entry->checker,
+ word->word_str,
+
suggestion_activated_cb,
+ gspell_entry);
+
+ gtk_menu_shell_prepend (GTK_MENU_SHELL (menu),
+ GTK_WIDGET (suggestions_menu_item));
+ }
+
+ _gspell_entry_word_free (word);
+}
+
+static void
+move_cursor_cb (GspellEntry *gspell_entry)
+{
+ _gspell_current_word_policy_cursor_moved (gspell_entry->current_word_policy);
+ recheck_all (gspell_entry);
+}
+
+static gboolean
+is_inside_word (const GSList *words,
+ gint char_pos)
+{
+ const GSList *l;
+
+ for (l = words; l != NULL; l = l->next)
+ {
+ const GspellEntryWord *cur_word = l->data;
+
+ if (cur_word->char_start <= char_pos && char_pos < cur_word->char_end)
+ {
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}
+
+static gboolean
+ends_word (const GSList *words,
+ gint char_pos)
+{
+ const GSList *l;
+
+ for (l = words; l != NULL; l = l->next)
+ {
+ const GspellEntryWord *cur_word = l->data;
+
+ if (cur_word->char_end == char_pos)
+ {
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}
+
+static void
+delete_text_before_cb (GtkEditable *editable,
+ gint start_pos,
+ gint end_pos,
+ GspellEntry *gspell_entry)
+{
+ gint real_start_pos;
+ gint real_end_pos;
+ gint cursor_pos;
+ GSList *words;
+ gboolean empty_selection;
+ gboolean spans_several_lines;
+ gboolean several_chars;
+ gboolean cursor_pos_at_start;
+ gboolean cursor_pos_at_end;
+ gboolean start_is_inside_word;
+ gboolean start_ends_word;
+ gboolean end_is_inside_word;
+ gboolean end_ends_word;
+
+ real_start_pos = start_pos;
+
+ if (end_pos < 0)
+ {
+ real_end_pos = gtk_entry_get_text_length (gspell_entry->entry);
+ }
+ else
+ {
+ real_end_pos = end_pos;
+ }
+
+ if (real_start_pos == real_end_pos)
+ {
+ return;
+ }
+
+ if (real_start_pos > real_end_pos)
+ {
+ gint real_start_pos_copy;
+
+ /* swap */
+ real_start_pos_copy = real_start_pos;
+ real_start_pos = real_end_pos;
+ real_end_pos = real_start_pos_copy;
+ }
+
+ g_assert_cmpint (real_start_pos, <, real_end_pos);
+
+ empty_selection = !gtk_editable_get_selection_bounds (editable, NULL, NULL);
+ spans_several_lines = FALSE;
+ several_chars = (real_end_pos - real_start_pos) > 1;
+
+ cursor_pos = gtk_editable_get_position (editable);
+ cursor_pos_at_start = cursor_pos == real_start_pos;
+ cursor_pos_at_end = cursor_pos == real_end_pos;
+
+ words = _gspell_entry_utils_get_words (gspell_entry->entry);
+
+ start_is_inside_word = is_inside_word (words, real_start_pos);
+ start_ends_word = ends_word (words, real_start_pos);
+
+ end_is_inside_word = is_inside_word (words, real_end_pos);
+ end_ends_word = ends_word (words, real_end_pos);
+
+ g_slist_free_full (words, _gspell_entry_word_free);
+
+ _gspell_current_word_policy_text_deleted (gspell_entry->current_word_policy,
+ empty_selection,
+ spans_several_lines,
+ several_chars,
+ cursor_pos_at_start,
+ cursor_pos_at_end,
+ start_is_inside_word,
+ start_ends_word,
+ end_is_inside_word,
+ end_ends_word);
+}
+
+static void
+set_entry (GspellEntry *gspell_entry,
+ GtkEntry *gtk_entry)
+{
+ g_return_if_fail (GTK_IS_ENTRY (gtk_entry));
+
+ g_assert (gspell_entry->entry == NULL);
+ gspell_entry->entry = gtk_entry;
+
+ g_signal_connect_after (gtk_entry,
+ "changed",
+ G_CALLBACK (changed_after_cb),
+ gspell_entry);
+
+ g_signal_connect (gtk_entry,
+ "notify::buffer",
+ G_CALLBACK (notify_buffer_cb),
+ gspell_entry);
+
+ g_assert (gspell_entry->notify_attributes_handler_id == 0);
+ gspell_entry->notify_attributes_handler_id =
+ g_signal_connect (gtk_entry,
+ "notify::attributes",
+ G_CALLBACK (notify_attributes_cb),
+ gspell_entry);
+
+ g_signal_connect (gtk_entry,
+ "popup-menu",
+ G_CALLBACK (popup_menu_cb),
+ gspell_entry);
+
+ g_signal_connect (gtk_entry,
+ "button-press-event",
+ G_CALLBACK (button_press_event_cb),
+ gspell_entry);
+
+ /* connect_after, so when menu items are prepended, they have more
+ * chances to be the first in the menu.
+ */
+ g_signal_connect_after (gtk_entry,
+ "populate-popup",
+ G_CALLBACK (populate_popup_cb),
+ gspell_entry);
+
+ /* What we want here is to be notified when the cursor moved, but _not_
+ * when the cursor moved because of a text insertion/deletion. To call
+ * _gspell_current_word_policy_cursor_moved().
+ *
+ * Connecting to notify::cursor-position is not suitable because we have
+ * notifications also when text is inserted/deleted. And we get the
+ * notification *after* the GtkEditable::insert-text signal (not
+ * *during* its emission). So it seems that the only simple solution is
+ * to connect to ::move-cursor, even if its documentation doesn't
+ * recommend that (normally, it should be used only to emit the signal).
+ *
+ * Note that _gspell_current_word_policy_cursor_moved() is also called
+ * in button_press_event_cb().
+ *
+ * The GtkEntry API is not really convenient, if you find a better
+ * solution, or if you improve the GtkEntry API...
+ */
+ g_signal_connect_swapped (gtk_entry,
+ "move-cursor",
+ G_CALLBACK (move_cursor_cb),
+ gspell_entry);
+
+ g_signal_connect (GTK_EDITABLE (gtk_entry),
+ "delete-text",
+ G_CALLBACK (delete_text_before_cb),
+ gspell_entry);
+
+ g_signal_connect_swapped (gtk_entry,
+ "notify::visibility",
+ G_CALLBACK (recheck_all),
+ gspell_entry);
+
+ update_buffer (gspell_entry);
+
+ g_object_notify (G_OBJECT (gspell_entry), "entry");
+}
+
+static void
+gspell_entry_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GspellEntry *gspell_entry = GSPELL_ENTRY (object);
+
+ switch (prop_id)
+ {
+ case PROP_ENTRY:
+ g_value_set_object (value, gspell_entry_get_entry (gspell_entry));
+ break;
+
+ case PROP_INLINE_SPELL_CHECKING:
+ g_value_set_boolean (value, gspell_entry_get_inline_spell_checking (gspell_entry));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gspell_entry_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GspellEntry *gspell_entry = GSPELL_ENTRY (object);
+
+ switch (prop_id)
+ {
+ case PROP_ENTRY:
+ set_entry (gspell_entry, g_value_get_object (value));
+ break;
+
+ case PROP_INLINE_SPELL_CHECKING:
+ gspell_entry_set_inline_spell_checking (gspell_entry, g_value_get_boolean (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gspell_entry_dispose (GObject *object)
+{
+ GspellEntry *gspell_entry = GSPELL_ENTRY (object);
+
+ gspell_entry->entry = NULL;
+ set_buffer (gspell_entry, NULL);
+ set_checker (gspell_entry, NULL);
+
+ if (gspell_entry->notify_attributes_idle_id != 0)
+ {
+ g_source_remove (gspell_entry->notify_attributes_idle_id);
+ gspell_entry->notify_attributes_idle_id = 0;
+ }
+
+ G_OBJECT_CLASS (gspell_entry_parent_class)->dispose (object);
+}
+
+static void
+gspell_entry_finalize (GObject *object)
+{
+ GspellEntry *gspell_entry = GSPELL_ENTRY (object);
+
+ /* Internal GObject, we can release it in finalize. */
+ g_clear_object (&gspell_entry->current_word_policy);
+
+ G_OBJECT_CLASS (gspell_entry_parent_class)->finalize (object);
+}
+
+static void
+gspell_entry_class_init (GspellEntryClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->get_property = gspell_entry_get_property;
+ object_class->set_property = gspell_entry_set_property;
+ object_class->dispose = gspell_entry_dispose;
+ object_class->finalize = gspell_entry_finalize;
+
+ /**
+ * GspellEntry:entry:
+ *
+ * The #GtkEntry.
+ *
+ * Since: 1.4
+ */
+ g_object_class_install_property (object_class,
+ PROP_ENTRY,
+ g_param_spec_object ("entry",
+ "Entry",
+ "",
+ GTK_TYPE_ENTRY,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * GspellEntry:inline-spell-checking:
+ *
+ * Whether the inline spell checking is enabled.
+ *
+ * Even if this property is %TRUE, #GspellEntry disables internally the
+ * inline spell checking in case the #GtkEntry:visibility property is
+ * %FALSE.
+ *
+ * Since: 1.4
+ */
+ g_object_class_install_property (object_class,
+ PROP_INLINE_SPELL_CHECKING,
+ g_param_spec_boolean ("inline-spell-checking",
+ "Inline Spell Checking",
+ "",
+ FALSE,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+}
+
+static void
+gspell_entry_init (GspellEntry *gspell_entry)
+{
+ gspell_entry->current_word_policy = _gspell_current_word_policy_new ();
+}
+
+/**
+ * gspell_entry_get_from_gtk_entry:
+ * @gtk_entry: a #GtkEntry.
+ *
+ * Returns the #GspellEntry of @gtk_entry. The returned object is guaranteed
+ * to be the same for the lifetime of @gtk_entry.
+ *
+ * Returns: (transfer none): the #GspellEntry of @gtk_entry.
+ * Since: 1.4
+ */
+GspellEntry *
+gspell_entry_get_from_gtk_entry (GtkEntry *gtk_entry)
+{
+ GspellEntry *gspell_entry;
+
+ g_return_val_if_fail (GTK_IS_ENTRY (gtk_entry), NULL);
+
+ gspell_entry = g_object_get_data (G_OBJECT (gtk_entry), GSPELL_ENTRY_KEY);
+
+ if (gspell_entry == NULL)
+ {
+ gspell_entry = g_object_new (GSPELL_TYPE_ENTRY,
+ "entry", gtk_entry,
+ NULL);
+
+ g_object_set_data_full (G_OBJECT (gtk_entry),
+ GSPELL_ENTRY_KEY,
+ gspell_entry,
+ g_object_unref);
+ }
+
+ g_return_val_if_fail (GSPELL_IS_ENTRY (gspell_entry), NULL);
+ return gspell_entry;
+}
+
+/**
+ * gspell_entry_basic_setup:
+ * @gspell_entry: a #GspellEntry.
+ *
+ * This function is a convenience function that does the following:
+ * - Set a spell checker. The language chosen is the one returned by
+ * gspell_language_get_default().
+ * - Set the #GspellEntry:inline-spell-checking property to %TRUE.
+ *
+ * Example:
+ * |[
+ * GtkEntry *gtk_entry;
+ * GspellEntry *gspell_entry;
+ *
+ * gspell_entry = gspell_entry_get_from_gtk_entry (gtk_entry);
+ * gspell_entry_basic_setup (gspell_entry);
+ * ]|
+ *
+ * This is equivalent to:
+ * |[
+ * GtkEntry *gtk_entry;
+ * GspellEntry *gspell_entry;
+ * GspellChecker *checker;
+ * GtkEntryBuffer *gtk_buffer;
+ * GspellEntryBuffer *gspell_buffer;
+ *
+ * checker = gspell_checker_new (NULL);
+ * gtk_buffer = gtk_entry_get_buffer (gtk_entry);
+ * gspell_buffer = gspell_entry_buffer_get_from_gtk_entry_buffer (gtk_buffer);
+ * gspell_entry_buffer_set_spell_checker (gspell_buffer, checker);
+ * g_object_unref (checker);
+ *
+ * gspell_entry = gspell_entry_get_from_gtk_entry (gtk_entry);
+ * gspell_entry_set_inline_spell_checking (gspell_entry, TRUE);
+ * ]|
+ *
+ * Since: 1.4
+ */
+void
+gspell_entry_basic_setup (GspellEntry *gspell_entry)
+{
+ GspellChecker *checker;
+ GtkEntryBuffer *gtk_buffer;
+ GspellEntryBuffer *gspell_buffer;
+
+ g_return_if_fail (GSPELL_IS_ENTRY (gspell_entry));
+
+ checker = gspell_checker_new (NULL);
+ gtk_buffer = gtk_entry_get_buffer (gspell_entry->entry);
+ gspell_buffer = gspell_entry_buffer_get_from_gtk_entry_buffer (gtk_buffer);
+ gspell_entry_buffer_set_spell_checker (gspell_buffer, checker);
+ g_object_unref (checker);
+
+ gspell_entry_set_inline_spell_checking (gspell_entry, TRUE);
+}
+
+/**
+ * gspell_entry_get_entry:
+ * @gspell_entry: a #GspellEntry.
+ *
+ * Returns: (transfer none): the #GtkEntry of @gspell_entry.
+ * Since: 1.4
+ */
+GtkEntry *
+gspell_entry_get_entry (GspellEntry *gspell_entry)
+{
+ g_return_val_if_fail (GSPELL_IS_ENTRY (gspell_entry), NULL);
+
+ return gspell_entry->entry;
+}
+
+/**
+ * gspell_entry_get_inline_spell_checking:
+ * @gspell_entry: a #GspellEntry.
+ *
+ * Returns: the value of the #GspellEntry:inline-spell-checking property.
+ * Since: 1.4
+ */
+gboolean
+gspell_entry_get_inline_spell_checking (GspellEntry *gspell_entry)
+{
+ g_return_val_if_fail (GSPELL_IS_ENTRY (gspell_entry), FALSE);
+
+ return gspell_entry->inline_spell_checking;
+}
+
+/**
+ * gspell_entry_set_inline_spell_checking:
+ * @gspell_entry: a #GspellEntry.
+ * @enable: the new state.
+ *
+ * Sets the #GspellEntry:inline-spell-checking property.
+ *
+ * Since: 1.4
+ */
+void
+gspell_entry_set_inline_spell_checking (GspellEntry *gspell_entry,
+ gboolean enable)
+{
+ g_return_if_fail (GSPELL_IS_ENTRY (gspell_entry));
+
+ enable = enable != FALSE;
+
+ if (gspell_entry->inline_spell_checking != enable)
+ {
+ gspell_entry->inline_spell_checking = enable;
+ recheck_all (gspell_entry);
+ g_object_notify (G_OBJECT (gspell_entry), "inline-spell-checking");
+ }
+}
+
+/* For unit tests.
+ * Returns: (transfer none) (element-type GspellEntryWord).
+ */
+const GSList *
+_gspell_entry_get_misspelled_words (GspellEntry *gspell_entry)
+{
+ g_return_val_if_fail (GSPELL_IS_ENTRY (gspell_entry), NULL);
+
+ return gspell_entry->misspelled_words;
+}
+
+/* ex:set ts=8 noet: */
diff --git a/gspell/gspell-entry.h b/gspell/gspell-entry.h
new file mode 100644
index 0000000..bd225ef
--- /dev/null
+++ b/gspell/gspell-entry.h
@@ -0,0 +1,59 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2016 - Sébastien Wilmet
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GSPELL_ENTRY_H
+#define GSPELL_ENTRY_H
+
+#if !defined (GSPELL_H_INSIDE) && !defined (GSPELL_COMPILATION)
+#error "Only <gspell/gspell.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+#include <gspell/gspell-version.h>
+
+G_BEGIN_DECLS
+
+#define GSPELL_TYPE_ENTRY (gspell_entry_get_type ())
+
+GSPELL_AVAILABLE_IN_1_4
+G_DECLARE_FINAL_TYPE (GspellEntry, gspell_entry,
+ GSPELL, ENTRY,
+ GObject)
+
+GSPELL_AVAILABLE_IN_1_4
+GspellEntry * gspell_entry_get_from_gtk_entry (GtkEntry *gtk_entry);
+
+GSPELL_AVAILABLE_IN_1_4
+void gspell_entry_basic_setup (GspellEntry *gspell_entry);
+
+GSPELL_AVAILABLE_IN_1_4
+GtkEntry * gspell_entry_get_entry (GspellEntry *gspell_entry);
+
+GSPELL_AVAILABLE_IN_1_4
+gboolean gspell_entry_get_inline_spell_checking (GspellEntry *gspell_entry);
+
+GSPELL_AVAILABLE_IN_1_4
+void gspell_entry_set_inline_spell_checking (GspellEntry *gspell_entry,
+ gboolean enable);
+
+G_END_DECLS
+
+#endif /* GSPELL_ENTRY_H */
+
+/* ex:set ts=8 noet: */
diff --git a/gspell/gspell-enum-types.c.template b/gspell/gspell-enum-types.c.template
new file mode 100644
index 0000000..477af65
--- /dev/null
+++ b/gspell/gspell-enum-types.c.template
@@ -0,0 +1,45 @@
+/*** BEGIN file-header ***/
+#if HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "gspell-enum-types.h"
+
+/*** END file-header ***/
+
+/*** BEGIN file-production ***/
+/* enumerations from "@filename@" */
+#include "@filename@"
+
+/*** END file-production ***/
+
+/*** BEGIN value-header ***/
+GType
+@enum_name@_get_type (void)
+{
+ static GType the_type = 0;
+
+ if (the_type == 0)
+ {
+ static const G@Type@Value values[] = {
+/*** END value-header ***/
+
+/*** BEGIN value-production ***/
+ { @VALUENAME@,
+ "@VALUENAME@",
+ "@valuenick@" },
+/*** END value-production ***/
+
+/*** BEGIN value-tail ***/
+ { 0, NULL, NULL }
+ };
+
+ the_type = g_@type@_register_static (
+ g_intern_static_string ("@EnumName@"),
+ values);
+ }
+
+ return the_type;
+}
+
+/*** END value-tail ***/
diff --git a/gspell/gspell-enum-types.h.template b/gspell/gspell-enum-types.h.template
new file mode 100644
index 0000000..5054d4b
--- /dev/null
+++ b/gspell/gspell-enum-types.h.template
@@ -0,0 +1,33 @@
+/*** BEGIN file-header ***/
+#ifndef GSPELL_ENUM_TYPES_H
+#define GSPELL_ENUM_TYPES_H
+
+#if !defined (GSPELL_H_INSIDE) && !defined (GSPELL_COMPILATION)
+#error "Only <gspell/gspell.h> can be included directly."
+#endif
+
+#include <glib-object.h>
+#include <gspell/gspell-version.h>
+
+G_BEGIN_DECLS
+
+/*** END file-header ***/
+
+/*** BEGIN file-production ***/
+/* Enumerations from "@filename@" */
+
+/*** END file-production ***/
+
+/*** BEGIN enumeration-production ***/
+#define GSPELL_TYPE_@ENUMSHORT@ (@enum_name@_get_type())
+
+_GSPELL_EXTERN
+GType @enum_name@_get_type (void);
+
+/*** END enumeration-production ***/
+
+/*** BEGIN file-tail ***/
+G_END_DECLS
+
+#endif /* GSPELL_ENUM_TYPES_H */
+/*** END file-tail ***/
diff --git a/gspell/gspell-icu.c b/gspell/gspell-icu.c
new file mode 100644
index 0000000..f5fbff5
--- /dev/null
+++ b/gspell/gspell-icu.c
@@ -0,0 +1,249 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2020 - Sébastien Wilmet <swilmet gnome org>
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "gspell-icu.h"
+#include <unicode/ustring.h>
+#include <unicode/uloc.h>
+
+/* Implementation notes:
+ *
+ * Before using the ICU library, the iso-codes package was used instead. It was
+ * fine on Linux since the iso-codes package is usually already installed. On
+ * MS Windows, to package an application the iso-codes package needed to be
+ * bundled with the app, which is not great because it's a quite sizeable dep.
+ *
+ * The ICU library is usually already installed too on Linux, and for Windows
+ * it's already an (indirect) dep of gedit (the Tepl library uses the ICU).
+ *
+ * MS Windows also provides a native API for something similar to
+ * _gspell_icu_get_language_name_from_code(). But it's easier to manage only one
+ * implementation (namely, based on the ICU library).
+ */
+
+/* Some of these functions are copied from the Tepl library. */
+
+/* Wrapper around u_strToUTF8() that handles the pre-flighting.
+ *
+ * Returns: (transfer full) (nullable): the newly-allocated string with the
+ * right size. Free with g_free() when no longer needed.
+ */
+static char *
+_gspell_icu_strToUTF8 (int32_t *pDestLength,
+ const UChar *src,
+ int32_t srcLength,
+ UErrorCode *pErrorCode)
+{
+ int32_t my_DestLength = 0;
+ UErrorCode my_ErrorCode = U_ZERO_ERROR;
+ char *dest = NULL;
+
+ u_strToUTF8 (NULL, 0, &my_DestLength,
+ src, srcLength,
+ &my_ErrorCode);
+
+ if (my_ErrorCode != U_BUFFER_OVERFLOW_ERROR &&
+ my_ErrorCode != U_STRING_NOT_TERMINATED_WARNING)
+ {
+ if (pDestLength != NULL)
+ {
+ *pDestLength = my_DestLength;
+ }
+ if (pErrorCode != NULL)
+ {
+ *pErrorCode = my_ErrorCode;
+ }
+
+ return NULL;
+ }
+
+ dest = g_malloc0 (my_DestLength + 1);
+
+ u_strToUTF8 (dest, my_DestLength + 1, pDestLength,
+ src, srcLength,
+ pErrorCode);
+
+ return dest;
+}
+
+/* Returns: (transfer full) (nullable): a nul-terminated UTF-8 string. Free with
+ * g_free() when no longer needed.
+ */
+static char *
+_gspell_icu_strToUTF8Simple (const UChar *uchars)
+{
+ char *utf8_str;
+ UErrorCode error_code = U_ZERO_ERROR;
+
+ utf8_str = _gspell_icu_strToUTF8 (NULL, uchars, -1, &error_code);
+
+ if (U_FAILURE (error_code))
+ {
+ g_free (utf8_str);
+ return NULL;
+ }
+
+ return utf8_str;
+}
+
+/* Wrapper around uloc_getDisplayName() that handles the pre-flighting.
+ *
+ * Returns: (transfer full) (nullable): the result as a newly-allocated buffer
+ * with the right size. Free with g_free() when no longer needed.
+ */
+static UChar *
+_gspell_icu_loc_getDisplayName (const char *localeID,
+ const char *inLocaleID,
+ UErrorCode *err)
+{
+ UChar *result = NULL;
+ int32_t result_size;
+ UErrorCode my_err = U_ZERO_ERROR;
+
+ result_size = uloc_getDisplayName (localeID,
+ inLocaleID,
+ NULL, 0,
+ &my_err);
+
+ if (my_err != U_BUFFER_OVERFLOW_ERROR &&
+ my_err != U_STRING_NOT_TERMINATED_WARNING)
+ {
+ if (err != NULL)
+ {
+ *err = my_err;
+ }
+
+ return NULL;
+ }
+
+ result = g_new0 (UChar, result_size + 1);
+
+ uloc_getDisplayName (localeID,
+ inLocaleID,
+ result, result_size + 1,
+ err);
+
+ return result;
+}
+
+/* Returns: (transfer full) (nullable): the result as a UTF-8 string. Free with
+ * g_free() when no longer needed.
+ */
+char *
+_gspell_icu_loc_getDisplayNameSimple (const char *localeID,
+ const char *inLocaleID)
+{
+ UChar *result;
+ char *utf8_result;
+ UErrorCode err = U_ZERO_ERROR;
+
+ result = _gspell_icu_loc_getDisplayName (localeID, inLocaleID, &err);
+
+ if (U_FAILURE (err))
+ {
+ g_free (result);
+ return NULL;
+ }
+
+ utf8_result = _gspell_icu_strToUTF8Simple (result);
+ g_free (result);
+ return utf8_result;
+}
+
+/* Wrapper around uloc_canonicalize() that handles the pre-flighting.
+ *
+ * Returns: (transfer full) (nullable): the result as a newly-allocated buffer
+ * with the right size. Free with g_free() when no longer needed.
+ */
+static char *
+_gspell_icu_loc_canonicalize (const char *localeID,
+ UErrorCode *err)
+{
+ char *result = NULL;
+ int32_t result_size;
+ UErrorCode my_err = U_ZERO_ERROR;
+
+ result_size = uloc_canonicalize (localeID, NULL, 0, &my_err);
+
+ if (my_err != U_BUFFER_OVERFLOW_ERROR &&
+ my_err != U_STRING_NOT_TERMINATED_WARNING)
+ {
+ if (err != NULL)
+ {
+ *err = my_err;
+ }
+
+ return NULL;
+ }
+
+ result = g_new0 (char, result_size + 1);
+ uloc_canonicalize (localeID, result, result_size + 1, err);
+ return result;
+}
+
+/* Returns: (transfer full) (nullable): the result, or %NULL in case of error.
+ * Free with g_free() when no longer needed.
+ */
+static char *
+_gspell_icu_loc_canonicalizeSimple (const char *localeID)
+{
+ char *result;
+ UErrorCode err = U_ZERO_ERROR;
+
+ result = _gspell_icu_loc_canonicalize (localeID, &err);
+
+ if (U_FAILURE (err))
+ {
+ g_free (result);
+ return NULL;
+ }
+
+ return result;
+}
+
+/* The name of this function uses gspell's terminology:
+ * "Language code": as in gspell_language_get_code().
+ * "Language name": as in gspell_language_get_name(), except that the
+ * translation is controlled by the @inLocaleID parameter which has the same
+ * meaning as for _gspell_icu_loc_getDisplayNameSimple(). If @inLocaleID is
+ * %NULL then the default locale is used to translate the name. The @inLocaleID
+ * parameter is kept for this function for unit tests.
+ *
+ * Returns: (transfer full) (nullable): the language name, or %NULL in case of
+ * error. Free with g_free() when no longer needed.
+ */
+char *
+_gspell_icu_get_language_name_from_code (const char *language_code,
+ const char *inLocaleID)
+{
+ char *canonicalized_language_code;
+ char *language_name;
+
+ /* language_code can come from an outside/foreign source, so it's better
+ * to pass it through level 2 canonicalization.
+ */
+ canonicalized_language_code = _gspell_icu_loc_canonicalizeSimple (language_code);
+ if (canonicalized_language_code == NULL)
+ {
+ return NULL;
+ }
+
+ language_name = _gspell_icu_loc_getDisplayNameSimple (canonicalized_language_code, inLocaleID);
+ g_free (canonicalized_language_code);
+ return language_name;
+}
diff --git a/gspell/gspell-icu.h b/gspell/gspell-icu.h
new file mode 100644
index 0000000..46a1c65
--- /dev/null
+++ b/gspell/gspell-icu.h
@@ -0,0 +1,39 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2020 - Sébastien Wilmet <swilmet gnome org>
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GSPELL_ICU_H
+#define GSPELL_ICU_H
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+G_GNUC_INTERNAL
+char * _gspell_icu_get_language_name_from_code (const char *language_code,
+ const char *inLocaleID);
+
+/* Intermediate functions, for unit tests: */
+
+G_GNUC_INTERNAL
+char * _gspell_icu_loc_getDisplayNameSimple (const char *localeID,
+ const char *inLocaleID);
+
+G_END_DECLS
+
+#endif /* GSPELL_ICU_H */
diff --git a/gspell/gspell-init.c b/gspell/gspell-init.c
new file mode 100644
index 0000000..683d2f0
--- /dev/null
+++ b/gspell/gspell-init.c
@@ -0,0 +1,170 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2015, 2016, 2020 - Sébastien Wilmet <swilmet gnome org>
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+/* Init i18n */
+
+/* Part of the code taken from the GtkSourceView library (gtksourceview-i18n.c).
+ * Copyright (C) 1997, 1998, 1999, 2000 Free Software Foundation
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <glib/gi18n-lib.h>
+#include "gconstructor.h"
+
+#ifdef G_OS_WIN32
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h>
+static HMODULE gspell_dll;
+#endif
+
+#ifdef OS_OSX
+#include <Cocoa/Cocoa.h>
+
+static gchar *
+dirs_os_x_get_bundle_resource_dir (void)
+{
+ NSAutoreleasePool *pool;
+ gchar *str = NULL;
+ NSString *path;
+
+ pool = [[NSAutoreleasePool alloc] init];
+
+ if ([[NSBundle mainBundle] bundleIdentifier] == nil)
+ {
+ [pool release];
+ return NULL;
+ }
+
+ path = [[NSBundle mainBundle] resourcePath];
+
+ if (!path)
+ {
+ [pool release];
+ return NULL;
+ }
+
+ str = g_strdup ([path UTF8String]);
+ [pool release];
+ return str;
+}
+
+static gchar *
+dirs_os_x_get_locale_dir (void)
+{
+ gchar *res_dir;
+ gchar *ret;
+
+ res_dir = dirs_os_x_get_bundle_resource_dir ();
+
+ if (res_dir == NULL)
+ {
+ ret = g_build_filename (DATADIR, "locale", NULL);
+ }
+ else
+ {
+ ret = g_build_filename (res_dir, "share", "locale", NULL);
+ g_free (res_dir);
+ }
+
+ return ret;
+}
+#endif /* OS_OSX */
+
+static gchar *
+get_locale_dir (void)
+{
+ gchar *locale_dir;
+
+#if defined (G_OS_WIN32)
+ gchar *win32_dir;
+
+ win32_dir = g_win32_get_package_installation_directory_of_module (gspell_dll);
+
+ locale_dir = g_build_filename (win32_dir, "share", "locale", NULL);
+
+ g_free (win32_dir);
+#elif defined (OS_OSX)
+ locale_dir = dirs_os_x_get_locale_dir ();
+#else
+ locale_dir = g_build_filename (DATADIR, "locale", NULL);
+#endif
+
+ return locale_dir;
+}
+
+static void
+gspell_init (void)
+{
+ gchar *locale_dir;
+
+ locale_dir = get_locale_dir ();
+ bindtextdomain (GETTEXT_PACKAGE, locale_dir);
+ g_free (locale_dir);
+
+ bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
+}
+
+#if defined (G_OS_WIN32)
+
+BOOL WINAPI DllMain (HINSTANCE hinstDLL,
+ DWORD fdwReason,
+ LPVOID lpvReserved);
+
+BOOL WINAPI
+DllMain (HINSTANCE hinstDLL,
+ DWORD fdwReason,
+ LPVOID lpvReserved)
+{
+ switch (fdwReason)
+ {
+ case DLL_PROCESS_ATTACH:
+ gspell_dll = hinstDLL;
+ gspell_init ();
+ break;
+
+ case DLL_THREAD_DETACH:
+ default:
+ /* do nothing */
+ break;
+ }
+
+ return TRUE;
+}
+
+#elif defined (G_HAS_CONSTRUCTORS)
+
+# ifdef G_DEFINE_CONSTRUCTOR_NEEDS_PRAGMA
+# pragma G_DEFINE_CONSTRUCTOR_PRAGMA_ARGS(gspell_constructor)
+# endif
+G_DEFINE_CONSTRUCTOR (gspell_constructor)
+
+static void
+gspell_constructor (void)
+{
+ gspell_init ();
+}
+
+#else
+# error Your platform/compiler is missing constructor support
+#endif
+
+/* ex:set ts=8 noet: */
diff --git a/gspell/gspell-inline-checker-text-buffer.c b/gspell/gspell-inline-checker-text-buffer.c
new file mode 100644
index 0000000..958a417
--- /dev/null
+++ b/gspell/gspell-inline-checker-text-buffer.c
@@ -0,0 +1,1485 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2002 - Paolo Maggi
+ * Copyright 2015, 2016, 2017 - Sébastien Wilmet
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+/* This is a modified version of GtkSpell 2.0.5 (gtkspell.sf.net)
+ * Copyright 2002 - Evan Martin
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "gspell-inline-checker-text-buffer.h"
+#include <string.h>
+#include <glib/gi18n-lib.h>
+#include "gspellregion.h"
+#include "gspell-checker.h"
+#include "gspell-context-menu.h"
+#include "gspell-current-word-policy.h"
+#include "gspell-text-buffer.h"
+#include "gspell-text-iter.h"
+#include "gspell-utils.h"
+
+struct _GspellInlineCheckerTextBuffer
+{
+ GObject parent;
+
+ GtkTextBuffer *buffer;
+ GspellChecker *spell_checker;
+
+ /* List of GtkTextView* */
+ GSList *views;
+
+ GtkTextTag *highlight_tag;
+ GtkTextTag *no_spell_check_tag;
+
+ GtkTextMark *mark_click;
+
+ GspellRegion *scan_region;
+ guint timeout_id;
+
+ GspellCurrentWordPolicy *current_word_policy;
+
+ /* If the unit test mode is enabled, there is no timeouts, and the whole
+ * buffer is scanned synchronously.
+ * The unit test mode tries to follow as most as possible the same code
+ * paths as the real code paths, otherwise the unit tests would be
+ * useless. As such, the function names reflect the real code paths.
+ */
+ guint unit_test_mode : 1;
+};
+
+enum
+{
+ PROP_0,
+ PROP_BUFFER,
+};
+
+typedef enum
+{
+ ADJUST_MODE_STRICTLY_INSIDE_WORD,
+ ADJUST_MODE_INCLUDE_NEIGHBORS,
+} AdjustMode;
+
+#define INLINE_CHECKER_TEXT_BUFFER_KEY "GspellInlineCheckerTextBufferID"
+
+/* Timeout durations in milliseconds. Writing and deleting text should be smooth
+ * and responsive.
+ */
+#define TIMEOUT_DURATION_BUFFER_MODIFIED 16
+#define TIMEOUT_DURATION_DRAWING 20
+
+#define PERF_DEBUG FALSE
+
+G_DEFINE_TYPE (GspellInlineCheckerTextBuffer, _gspell_inline_checker_text_buffer, G_TYPE_OBJECT)
+
+/* Remove the highlight_tag only if present. If gtk_text_buffer_remove_tag() is
+ * called when the tag is not present, GtkTextView anyway queues a redraw, which
+ * we want to avoid (it can lead to an infinite loop).
+ */
+static void
+remove_highlight_tag_if_present (GspellInlineCheckerTextBuffer *spell,
+ const GtkTextIter *start,
+ const GtkTextIter *end)
+{
+ gboolean remove = FALSE;
+
+ if (gtk_text_iter_has_tag (start, spell->highlight_tag))
+ {
+ remove = TRUE;
+ }
+ else
+ {
+ GtkTextIter iter = *start;
+
+ if (gtk_text_iter_forward_to_tag_toggle (&iter, spell->highlight_tag) &&
+ gtk_text_iter_compare (&iter, end) < 0)
+ {
+ remove = TRUE;
+ }
+ }
+
+ if (remove)
+ {
+ gtk_text_buffer_remove_tag (spell->buffer,
+ spell->highlight_tag,
+ start,
+ end);
+ }
+}
+
+static void
+adjust_iters (GtkTextIter *start,
+ GtkTextIter *end,
+ AdjustMode mode)
+{
+ switch (mode)
+ {
+ case ADJUST_MODE_STRICTLY_INSIDE_WORD:
+ if (_gspell_text_iter_inside_word (start) &&
+ !_gspell_text_iter_starts_word (start))
+ {
+ _gspell_text_iter_backward_word_start (start);
+ }
+
+ if (_gspell_text_iter_inside_word (end) &&
+ !_gspell_text_iter_starts_word (end))
+ {
+ _gspell_text_iter_forward_word_end (end);
+ }
+ break;
+
+ case ADJUST_MODE_INCLUDE_NEIGHBORS:
+ if (_gspell_text_iter_ends_word (start) ||
+ (_gspell_text_iter_inside_word (start) &&
+ !_gspell_text_iter_starts_word (start)))
+ {
+ _gspell_text_iter_backward_word_start (start);
+ }
+
+ if (_gspell_text_iter_inside_word (end))
+ {
+ _gspell_text_iter_forward_word_end (end);
+ }
+ break;
+
+ default:
+ g_assert_not_reached ();
+ }
+}
+
+/* Free *attrs with g_free() when no longer needed. */
+static void
+get_pango_log_attrs (const gchar *text,
+ PangoLogAttr **attrs,
+ gint *n_attrs)
+{
+ *n_attrs = g_utf8_strlen (text, -1) + 1;
+ *attrs = g_new0 (PangoLogAttr, *n_attrs);
+
+ pango_get_log_attrs (text,
+ strlen (text),
+ -1,
+ NULL,
+ *attrs,
+ *n_attrs);
+
+ _gspell_utils_improve_word_boundaries (text, *attrs, *n_attrs);
+}
+
+static gboolean
+should_apply_tag_to_misspelled_word (GspellInlineCheckerTextBuffer *spell,
+ const GtkTextIter *word_start,
+ const GtkTextIter *word_end)
+{
+ GtkTextIter iter;
+
+ if (spell->no_spell_check_tag == NULL)
+ {
+ return TRUE;
+ }
+
+ if (gtk_text_iter_has_tag (word_start, spell->no_spell_check_tag))
+ {
+ return FALSE;
+ }
+
+ iter = *word_start;
+ if (!gtk_text_iter_forward_to_tag_toggle (&iter, spell->no_spell_check_tag))
+ {
+ return TRUE;
+ }
+
+ return gtk_text_iter_compare (word_end, &iter) <= 0;
+}
+
+/* A first implementation of this function used the _gspell_text_iter_*()
+ * functions in a loop to navigate through words between @start and @end.
+ * But the _gspell_text_iter_*() functions are *slow*. So a new implementation
+ * has been written to reduce the number of calls to GtkTextView functions, and
+ * it's up to 20x faster! (200 ms -> 10 ms).
+ * And there is most probably still room for performance improvements.
+ */
+static void
+check_subregion (GspellInlineCheckerTextBuffer *spell,
+ GtkTextIter *start,
+ GtkTextIter *end)
+{
+ gchar *text;
+ const gchar *cur_text_pos;
+ const gchar *word_start;
+ gint word_start_char_pos;
+ PangoLogAttr *attrs;
+ gint n_attrs;
+ gint attr_num;
+ gint start_offset;
+
+ g_return_if_fail (gtk_text_iter_compare (start, end) <= 0);
+
+ adjust_iters (start, end, ADJUST_MODE_STRICTLY_INSIDE_WORD);
+
+ gtk_text_buffer_remove_tag (spell->buffer,
+ spell->highlight_tag,
+ start,
+ end);
+
+ if (spell->spell_checker == NULL ||
+ gspell_checker_get_language (spell->spell_checker) == NULL)
+ {
+ return;
+ }
+
+ text = gtk_text_iter_get_slice (start, end);
+
+ if (text == NULL || text[0] == '\0')
+ {
+ g_free (text);
+ return;
+ }
+
+ get_pango_log_attrs (text, &attrs, &n_attrs);
+
+ attr_num = 0;
+ cur_text_pos = text;
+ word_start = NULL;
+ word_start_char_pos = 0;
+
+ start_offset = gtk_text_iter_get_offset (start);
+
+ while (attr_num < n_attrs)
+ {
+ PangoLogAttr *cur_attr = &attrs[attr_num];
+
+ if (word_start != NULL &&
+ cur_attr->is_word_end)
+ {
+ gint word_byte_length;
+ gboolean misspelled;
+ GError *error = NULL;
+
+ if (cur_text_pos != NULL)
+ {
+ word_byte_length = cur_text_pos - word_start;
+ }
+ else
+ {
+ word_byte_length = -1;
+ }
+
+ misspelled = !gspell_checker_check_word (spell->spell_checker,
+ word_start,
+ word_byte_length,
+ &error);
+
+ if (error != NULL)
+ {
+ g_warning ("Inline spell checker: %s", error->message);
+ g_clear_error (&error);
+ }
+
+ if (misspelled)
+ {
+ gint word_start_offset;
+ gint word_end_offset;
+ GtkTextIter word_start_iter;
+ GtkTextIter word_end_iter;
+
+ word_start_offset = start_offset + word_start_char_pos;
+ word_end_offset = start_offset + attr_num;
+
+ gtk_text_buffer_get_iter_at_offset (spell->buffer,
+ &word_start_iter,
+ word_start_offset);
+
+ gtk_text_buffer_get_iter_at_offset (spell->buffer,
+ &word_end_iter,
+ word_end_offset);
+
+ /* FIXME: it's a bit stupid to spell-check words
+ * in the no-spell-check region. The relevant
+ * word boundaries in the PangoLogAttr array
+ * should be removed beforehand.
+ */
+ if (should_apply_tag_to_misspelled_word (spell, &word_start_iter,
&word_end_iter))
+ {
+ gtk_text_buffer_apply_tag (spell->buffer,
+ spell->highlight_tag,
+ &word_start_iter,
+ &word_end_iter);
+ }
+ }
+
+ /* Find next word start. */
+ word_start = NULL;
+ }
+
+ if (word_start == NULL &&
+ cur_attr->is_word_start)
+ {
+ word_start = cur_text_pos;
+ word_start_char_pos = attr_num;
+ }
+
+ if (attr_num == n_attrs - 1 ||
+ cur_text_pos == NULL ||
+ cur_text_pos[0] == '\0')
+ {
+ break;
+ }
+
+ attr_num++;
+ cur_text_pos = g_utf8_find_next_char (cur_text_pos, NULL);
+ }
+
+ /* Sanity checks */
+
+ if (attr_num != n_attrs - 1)
+ {
+ g_warning ("%s(): problem in loop iteration, attr_num=%d but should be %d. "
+ "End of string reached too early.",
+ G_STRFUNC,
+ attr_num,
+ n_attrs - 1);
+ }
+
+ if (cur_text_pos != NULL && cur_text_pos[0] != '\0')
+ {
+ g_warning ("%s(): end of string not reached.", G_STRFUNC);
+ }
+
+ g_free (text);
+ g_free (attrs);
+}
+
+static void
+get_visible_region (GtkTextView *view,
+ GtkTextIter *start,
+ GtkTextIter *end)
+{
+ GdkRectangle visible_rect;
+
+ gtk_text_view_get_visible_rect (view, &visible_rect);
+
+ gtk_text_view_get_line_at_y (view,
+ start,
+ visible_rect.y,
+ NULL);
+
+ gtk_text_view_get_line_at_y (view,
+ end,
+ visible_rect.y + visible_rect.height,
+ NULL);
+
+ gtk_text_iter_backward_line (start);
+ gtk_text_iter_forward_line (end);
+}
+
+/* Returns TRUE if there is a current word. */
+static gboolean
+get_current_word_boundaries (GtkTextBuffer *buffer,
+ GtkTextIter *current_word_start,
+ GtkTextIter *current_word_end)
+{
+ GtkTextIter insert_iter;
+
+ g_assert (current_word_start != NULL);
+ g_assert (current_word_end != NULL);
+
+ if (gtk_text_buffer_get_has_selection (buffer))
+ {
+ return FALSE;
+ }
+
+ gtk_text_buffer_get_iter_at_mark (buffer,
+ &insert_iter,
+ gtk_text_buffer_get_insert (buffer));
+
+ *current_word_start = insert_iter;
+ *current_word_end = insert_iter;
+
+ adjust_iters (current_word_start, current_word_end, ADJUST_MODE_INCLUDE_NEIGHBORS);
+
+ return (!gtk_text_iter_equal (current_word_start, &insert_iter) ||
+ !gtk_text_iter_equal (current_word_end, &insert_iter));
+}
+
+static void
+check_visible_region_in_view (GspellInlineCheckerTextBuffer *spell,
+ GtkTextView *view)
+{
+ GtkTextIter visible_start;
+ GtkTextIter visible_end;
+ GspellRegion *intersect;
+ GspellRegionIter intersect_iter;
+
+ if (spell->scan_region == NULL)
+ {
+ return;
+ }
+
+ if (view != NULL)
+ {
+ get_visible_region (view, &visible_start, &visible_end);
+ }
+ else
+ {
+ g_assert (spell->unit_test_mode);
+ gtk_text_buffer_get_bounds (spell->buffer, &visible_start, &visible_end);
+ }
+
+ intersect = _gspell_region_intersect_subregion (spell->scan_region,
+ &visible_start,
+ &visible_end);
+
+ if (_gspell_region_is_empty (intersect))
+ {
+ g_clear_object (&intersect);
+ return;
+ }
+
+ if (!_gspell_current_word_policy_get_check_current_word (spell->current_word_policy))
+ {
+ GtkTextIter current_word_start;
+ GtkTextIter current_word_end;
+
+ if (get_current_word_boundaries (spell->buffer,
+ ¤t_word_start,
+ ¤t_word_end))
+ {
+ remove_highlight_tag_if_present (spell,
+ ¤t_word_start,
+ ¤t_word_end);
+
+ _gspell_region_subtract_subregion (intersect,
+ ¤t_word_start,
+ ¤t_word_end);
+
+ /* Be sure that the current word will be re-checked
+ * later when it will no longer be the current word.
+ */
+ _gspell_region_add_subregion (spell->scan_region,
+ ¤t_word_start,
+ ¤t_word_end);
+
+ if (_gspell_region_is_empty (intersect))
+ {
+ g_clear_object (&intersect);
+ return;
+ }
+ }
+ }
+
+ _gspell_region_get_start_region_iter (intersect, &intersect_iter);
+
+ while (!_gspell_region_iter_is_end (&intersect_iter))
+ {
+ GtkTextIter start;
+ GtkTextIter end;
+ GtkTextIter orig_start;
+ GtkTextIter orig_end;
+ gboolean bug = FALSE;
+
+ if (!_gspell_region_iter_get_subregion (&intersect_iter, &start, &end))
+ {
+ break;
+ }
+
+ orig_start = start;
+ orig_end = end;
+
+ {
+#if PERF_DEBUG
+ GTimer *timer;
+
+ g_print ("check_subregion [%d, %d]\n",
+ gtk_text_iter_get_offset (&start),
+ gtk_text_iter_get_offset (&end));
+
+ timer = g_timer_new ();
+#endif
+
+ check_subregion (spell, &start, &end);
+
+#if PERF_DEBUG
+ g_print ("check_subregion took %lf ms.\n\n",
+ 1000 * g_timer_elapsed (timer, NULL));
+ g_timer_destroy (timer);
+#endif
+ }
+
+ /* Ensure that we don't have an infinite loop. We must subtract
+ * from scan_region at least [orig_start, orig_end], otherwise
+ * we will re-check the same subregion again and again.
+ */
+ if (gtk_text_iter_compare (&orig_start, &start) < 0)
+ {
+ g_warning ("Should not reach this code path.");
+ bug = TRUE;
+ start = orig_start;
+ }
+ if (gtk_text_iter_compare (&end, &orig_end) < 0)
+ {
+ g_warning ("Should not reach this code path.");
+ bug = TRUE;
+ end = orig_end;
+ }
+
+ if (bug)
+ {
+ gchar *text;
+
+ text = gtk_text_iter_get_slice (&start, &end);
+ g_warning ("Text that caused the bug: '%s'", text);
+ g_warning ("Please report the bug to: " PACKAGE_BUGREPORT);
+ g_free (text);
+ }
+
+ _gspell_region_subtract_subregion (spell->scan_region, &start, &end);
+
+ _gspell_region_iter_next (&intersect_iter);
+ }
+
+ g_clear_object (&intersect);
+
+ if (_gspell_region_is_empty (spell->scan_region))
+ {
+ g_clear_object (&spell->scan_region);
+ }
+}
+
+static void
+check_visible_region (GspellInlineCheckerTextBuffer *spell)
+{
+ GSList *l;
+
+ if (spell->scan_region == NULL)
+ {
+ return;
+ }
+
+ if (spell->unit_test_mode)
+ {
+ check_visible_region_in_view (spell, NULL);
+ return;
+ }
+
+ for (l = spell->views; l != NULL; l = l->next)
+ {
+ GtkTextView *view = GTK_TEXT_VIEW (l->data);
+ check_visible_region_in_view (spell, view);
+ }
+}
+
+static gboolean
+timeout_cb (GspellInlineCheckerTextBuffer *spell)
+{
+ check_visible_region (spell);
+
+ spell->timeout_id = 0;
+ return G_SOURCE_REMOVE;
+}
+
+static void
+install_timeout (GspellInlineCheckerTextBuffer *spell,
+ guint duration)
+{
+ if (spell->timeout_id != 0)
+ {
+ g_source_remove (spell->timeout_id);
+ spell->timeout_id = 0;
+ }
+
+ if (spell->unit_test_mode)
+ {
+ timeout_cb (spell);
+ }
+ else
+ {
+ spell->timeout_id = g_timeout_add (duration,
+ (GSourceFunc) timeout_cb,
+ spell);
+ }
+}
+
+static void
+add_subregion_to_scan (GspellInlineCheckerTextBuffer *spell,
+ const GtkTextIter *start,
+ const GtkTextIter *end)
+{
+ if (spell->scan_region == NULL)
+ {
+ spell->scan_region = _gspell_region_new (spell->buffer);
+ }
+
+ _gspell_region_add_subregion (spell->scan_region, start, end);
+}
+
+static void
+recheck_all (GspellInlineCheckerTextBuffer *spell)
+{
+ GtkTextIter start;
+ GtkTextIter end;
+
+ gtk_text_buffer_get_bounds (spell->buffer, &start, &end);
+ add_subregion_to_scan (spell, &start, &end);
+
+ check_visible_region (spell);
+}
+
+/* The word boundaries are not necessarily the same before and after a text
+ * insertion or deletion. We need the broader boundaries, so we connect to the
+ * signal without and with the AFTER flag.
+ */
+static void
+insert_text_before_cb (GtkTextBuffer *buffer,
+ GtkTextIter *location,
+ gchar *text,
+ gint length,
+ GspellInlineCheckerTextBuffer *spell)
+{
+ GtkTextIter start;
+ GtkTextIter end;
+
+ start = *location;
+ end = *location;
+ adjust_iters (&start, &end, ADJUST_MODE_INCLUDE_NEIGHBORS);
+ add_subregion_to_scan (spell, &start, &end);
+
+ /* Don't install_timeout(), it will anyway be called in
+ * insert_text_after_cb(). If install_timeout() is called here, it is a
+ * problem for the unit test mode because the subregion would be scanned
+ * directly, but we need to wait that the text is inserted, otherwise
+ * this can give different results (since the word boundaries are not
+ * necessarily the same).
+ */
+}
+
+static void
+insert_text_after_cb (GtkTextBuffer *buffer,
+ GtkTextIter *location,
+ gchar *text,
+ gint length,
+ GspellInlineCheckerTextBuffer *spell)
+{
+ glong n_chars;
+ GtkTextIter start;
+ GtkTextIter end;
+
+ n_chars = g_utf8_strlen (text, length);
+
+ start = *location;
+ end = *location;
+ gtk_text_iter_backward_chars (&start, n_chars);
+
+ adjust_iters (&start, &end, ADJUST_MODE_INCLUDE_NEIGHBORS);
+ add_subregion_to_scan (spell, &start, &end);
+
+ /* Check current word? */
+ if (n_chars > 1)
+ {
+ _gspell_current_word_policy_several_chars_inserted (spell->current_word_policy);
+ }
+ else
+ {
+ gunichar ch;
+ gboolean empty_selection;
+ GtkTextIter cursor_pos;
+ gboolean at_cursor_pos;
+
+ ch = g_utf8_get_char (text);
+ empty_selection = !gtk_text_buffer_get_has_selection (buffer);
+
+ gtk_text_buffer_get_iter_at_mark (buffer,
+ &cursor_pos,
+ gtk_text_buffer_get_insert (buffer));
+
+ at_cursor_pos = gtk_text_iter_equal (location, &cursor_pos);
+
+ _gspell_current_word_policy_single_char_inserted (spell->current_word_policy,
+ ch,
+ empty_selection,
+ at_cursor_pos);
+ }
+
+ install_timeout (spell, TIMEOUT_DURATION_BUFFER_MODIFIED);
+}
+
+/* Same reasoning as for the ::insert-text signal. */
+static void
+delete_range_before_cb (GtkTextBuffer *buffer,
+ GtkTextIter *start,
+ GtkTextIter *end,
+ GspellInlineCheckerTextBuffer *spell)
+{
+ {
+ GtkTextIter start_adjusted;
+ GtkTextIter end_adjusted;
+
+ start_adjusted = *start;
+ end_adjusted = *end;
+ adjust_iters (&start_adjusted, &end_adjusted, ADJUST_MODE_INCLUDE_NEIGHBORS);
+ add_subregion_to_scan (spell, &start_adjusted, &end_adjusted);
+ }
+
+ /* Check current word? */
+ {
+ gboolean empty_selection;
+ gboolean spans_several_lines;
+ gboolean several_chars;
+ GtkTextIter cursor_pos;
+ gboolean cursor_pos_at_start;
+ gboolean cursor_pos_at_end;
+ gboolean start_is_inside_word;
+ gboolean start_ends_word;
+ gboolean end_is_inside_word;
+ gboolean end_ends_word;
+
+ empty_selection = !gtk_text_buffer_get_has_selection (buffer);
+ spans_several_lines = gtk_text_iter_get_line (start) != gtk_text_iter_get_line (end);
+ several_chars = gtk_text_iter_get_offset (end) - gtk_text_iter_get_offset (start) > 1;
+
+ gtk_text_buffer_get_iter_at_mark (buffer,
+ &cursor_pos,
+ gtk_text_buffer_get_insert (buffer));
+
+ cursor_pos_at_start = gtk_text_iter_equal (&cursor_pos, start);
+ cursor_pos_at_end = gtk_text_iter_equal (&cursor_pos, end);
+
+ start_is_inside_word = _gspell_text_iter_inside_word (start);
+ start_ends_word = _gspell_text_iter_ends_word (start);
+ end_is_inside_word = _gspell_text_iter_inside_word (end);
+ end_ends_word = _gspell_text_iter_ends_word (end);
+
+ _gspell_current_word_policy_text_deleted (spell->current_word_policy,
+ empty_selection,
+ spans_several_lines,
+ several_chars,
+ cursor_pos_at_start,
+ cursor_pos_at_end,
+ start_is_inside_word,
+ start_ends_word,
+ end_is_inside_word,
+ end_ends_word);
+ }
+}
+
+static void
+delete_range_after_cb (GtkTextBuffer *buffer,
+ GtkTextIter *start,
+ GtkTextIter *end,
+ GspellInlineCheckerTextBuffer *spell)
+{
+ GtkTextIter start_adjusted;
+ GtkTextIter end_adjusted;
+
+ g_return_if_fail (gtk_text_iter_equal (start, end));
+
+ start_adjusted = *start;
+ end_adjusted = *end;
+ adjust_iters (&start_adjusted, &end_adjusted, ADJUST_MODE_INCLUDE_NEIGHBORS);
+ add_subregion_to_scan (spell, &start_adjusted, &end_adjusted);
+
+ install_timeout (spell, TIMEOUT_DURATION_BUFFER_MODIFIED);
+}
+
+static void
+mark_set_after_cb (GtkTextBuffer *buffer,
+ GtkTextIter *location,
+ GtkTextMark *mark,
+ GspellInlineCheckerTextBuffer *spell)
+{
+ if (mark == gtk_text_buffer_get_insert (buffer))
+ {
+ _gspell_current_word_policy_cursor_moved (spell->current_word_policy);
+ install_timeout (spell, TIMEOUT_DURATION_BUFFER_MODIFIED);
+ }
+}
+
+static gboolean
+get_word_extents_at_click_position (GspellInlineCheckerTextBuffer *spell,
+ GtkTextIter *start,
+ GtkTextIter *end)
+{
+ GtkTextIter iter;
+
+ gtk_text_buffer_get_iter_at_mark (spell->buffer, &iter, spell->mark_click);
+
+ if (!_gspell_text_iter_inside_word (&iter) &&
+ !_gspell_text_iter_ends_word (&iter))
+ {
+ return FALSE;
+ }
+
+ *start = iter;
+ if (!_gspell_text_iter_starts_word (start))
+ {
+ _gspell_text_iter_backward_word_start (start);
+ }
+
+ *end = iter;
+ if (!_gspell_text_iter_ends_word (end))
+ {
+ _gspell_text_iter_forward_word_end (end);
+ }
+
+ return TRUE;
+}
+
+static void
+suggestion_activated_cb (const gchar *suggested_word,
+ gpointer user_data)
+{
+ GspellInlineCheckerTextBuffer *spell;
+ GtkTextIter start;
+ GtkTextIter end;
+ gchar *old_word;
+
+ g_return_if_fail (GSPELL_IS_INLINE_CHECKER_TEXT_BUFFER (user_data));
+
+ spell = GSPELL_INLINE_CHECKER_TEXT_BUFFER (user_data);
+
+ if (!get_word_extents_at_click_position (spell, &start, &end))
+ {
+ return;
+ }
+
+ old_word = gtk_text_buffer_get_text (spell->buffer, &start, &end, FALSE);
+
+ gtk_text_buffer_begin_user_action (spell->buffer);
+
+ gtk_text_buffer_delete (spell->buffer, &start, &end);
+ gtk_text_buffer_insert (spell->buffer, &start, suggested_word, -1);
+
+ gtk_text_buffer_end_user_action (spell->buffer);
+
+ if (spell->spell_checker != NULL)
+ {
+ gspell_checker_set_correction (spell->spell_checker,
+ old_word, -1,
+ suggested_word, -1);
+ }
+
+ g_free (old_word);
+}
+
+void
+_gspell_inline_checker_text_buffer_populate_popup (GspellInlineCheckerTextBuffer *spell,
+ GtkMenu *menu)
+{
+ GtkMenuItem *menu_item;
+ GtkTextIter start;
+ GtkTextIter end;
+ gchar *misspelled_word;
+
+ if (!get_word_extents_at_click_position (spell, &start, &end))
+ {
+ return;
+ }
+
+ if (!gtk_text_iter_has_tag (&start, spell->highlight_tag))
+ {
+ return;
+ }
+
+ if (spell->spell_checker == NULL)
+ {
+ return;
+ }
+
+ /* Prepend suggestions */
+
+ misspelled_word = gtk_text_buffer_get_text (spell->buffer, &start, &end, FALSE);
+
+ menu_item = _gspell_context_menu_get_suggestions_menu_item (spell->spell_checker,
+ misspelled_word,
+ suggestion_activated_cb,
+ spell);
+
+ gtk_menu_shell_prepend (GTK_MENU_SHELL (menu),
+ GTK_WIDGET (menu_item));
+
+ g_free (misspelled_word);
+}
+
+static gboolean
+draw_cb (GtkWidget *text_view,
+ cairo_t *cr,
+ GspellInlineCheckerTextBuffer *spell)
+{
+ install_timeout (spell, TIMEOUT_DURATION_DRAWING);
+
+ return GDK_EVENT_PROPAGATE;
+}
+
+static void
+remove_tag_to_word (GspellInlineCheckerTextBuffer *spell,
+ const gchar *word)
+{
+ GtkTextIter iter;
+
+ gtk_text_buffer_get_start_iter (spell->buffer, &iter);
+
+ while (TRUE)
+ {
+ gboolean found;
+ GtkTextIter match_start;
+ GtkTextIter match_end;
+
+ found = gtk_text_iter_forward_search (&iter,
+ word,
+ GTK_TEXT_SEARCH_VISIBLE_ONLY |
+ GTK_TEXT_SEARCH_TEXT_ONLY,
+ &match_start,
+ &match_end,
+ NULL);
+
+ if (!found)
+ {
+ break;
+ }
+
+ if (_gspell_text_iter_starts_word (&match_start) &&
+ _gspell_text_iter_ends_word (&match_end))
+ {
+ gtk_text_buffer_remove_tag (spell->buffer,
+ spell->highlight_tag,
+ &match_start,
+ &match_end);
+ }
+
+ iter = match_end;
+ }
+}
+
+static void
+word_added_cb (GspellChecker *checker,
+ const gchar *word,
+ GspellInlineCheckerTextBuffer *spell)
+{
+ remove_tag_to_word (spell, word);
+}
+
+static void
+session_cleared_cb (GspellChecker *checker,
+ GspellInlineCheckerTextBuffer *spell)
+{
+ _gspell_current_word_policy_session_cleared (spell->current_word_policy);
+ recheck_all (spell);
+}
+
+static void
+language_notify_cb (GspellChecker *checker,
+ GParamSpec *pspec,
+ GspellInlineCheckerTextBuffer *spell)
+{
+ _gspell_current_word_policy_language_changed (spell->current_word_policy);
+ recheck_all (spell);
+}
+
+/* When the user right-clicks on a word, they want to check that word.
+ * Here, we do NOT move the cursor to the location of the clicked-upon word
+ * since that prevents the use of edit functions on the context menu.
+ */
+static gboolean
+button_press_event_cb (GtkTextView *view,
+ GdkEventButton *event,
+ GspellInlineCheckerTextBuffer *spell)
+{
+ if (event->button == GDK_BUTTON_SECONDARY)
+ {
+ GtkTextBuffer *buffer = gtk_text_view_get_buffer (view);
+ GtkTextIter iter;
+ gint x;
+ gint y;
+
+ gtk_text_view_window_to_buffer_coords (view,
+ GTK_TEXT_WINDOW_TEXT,
+ event->x, event->y,
+ &x, &y);
+
+ gtk_text_view_get_iter_at_location (view, &iter, x, y);
+
+ gtk_text_buffer_move_mark (buffer, spell->mark_click, &iter);
+ }
+
+ return GDK_EVENT_PROPAGATE;
+}
+
+/* Move the insert mark before popping up the menu, otherwise it
+ * will contain the wrong set of suggestions.
+ */
+static gboolean
+popup_menu_cb (GtkTextView *view,
+ GspellInlineCheckerTextBuffer *spell)
+{
+ GtkTextIter iter;
+
+ gtk_text_buffer_get_iter_at_mark (spell->buffer, &iter,
+ gtk_text_buffer_get_insert (spell->buffer));
+ gtk_text_buffer_move_mark (spell->buffer, spell->mark_click, &iter);
+
+ return FALSE;
+}
+
+static void
+apply_or_remove_tag_cb (GtkTextBuffer *buffer,
+ GtkTextTag *tag,
+ GtkTextIter *start,
+ GtkTextIter *end,
+ GspellInlineCheckerTextBuffer *spell)
+{
+ if (spell->no_spell_check_tag != NULL &&
+ spell->no_spell_check_tag == tag)
+ {
+ add_subregion_to_scan (spell, start, end);
+ install_timeout (spell, TIMEOUT_DURATION_BUFFER_MODIFIED);
+ }
+}
+
+static void
+tag_added_cb (GtkTextTagTable *table,
+ GtkTextTag *tag,
+ GspellInlineCheckerTextBuffer *spell)
+{
+ gchar *name;
+
+ g_object_get (tag, "name", &name, NULL);
+
+ if (g_strcmp0 (name, "gtksourceview:context-classes:no-spell-check") == 0)
+ {
+ g_return_if_fail (spell->no_spell_check_tag == NULL);
+
+ spell->no_spell_check_tag = g_object_ref (tag);
+
+ _gspell_current_word_policy_set_check_current_word (spell->current_word_policy, TRUE);
+ recheck_all (spell);
+ }
+
+ g_free (name);
+}
+
+static void
+tag_removed_cb (GtkTextTagTable *table,
+ GtkTextTag *tag,
+ GspellInlineCheckerTextBuffer *spell)
+{
+ if (spell->no_spell_check_tag != NULL &&
+ spell->no_spell_check_tag == tag)
+ {
+ g_clear_object (&spell->no_spell_check_tag);
+
+ _gspell_current_word_policy_set_check_current_word (spell->current_word_policy, TRUE);
+ recheck_all (spell);
+ }
+}
+
+static void
+set_spell_checker (GspellInlineCheckerTextBuffer *spell,
+ GspellChecker *checker)
+{
+ g_return_if_fail (checker == NULL || GSPELL_IS_CHECKER (checker));
+
+ if (spell->spell_checker == checker)
+ {
+ return;
+ }
+
+ if (spell->spell_checker != NULL)
+ {
+ g_signal_handlers_disconnect_by_data (spell->spell_checker, spell);
+ g_object_unref (spell->spell_checker);
+ }
+
+ spell->spell_checker = checker;
+
+ if (spell->spell_checker != NULL)
+ {
+ g_object_ref (spell->spell_checker);
+
+ g_signal_connect (spell->spell_checker,
+ "word-added-to-session",
+ G_CALLBACK (word_added_cb),
+ spell);
+
+ g_signal_connect (spell->spell_checker,
+ "word-added-to-personal",
+ G_CALLBACK (word_added_cb),
+ spell);
+
+ g_signal_connect (spell->spell_checker,
+ "session-cleared",
+ G_CALLBACK (session_cleared_cb),
+ spell);
+
+ g_signal_connect (spell->spell_checker,
+ "notify::language",
+ G_CALLBACK (language_notify_cb),
+ spell);
+ }
+}
+
+static void
+spell_checker_notify_cb (GspellTextBuffer *gspell_buffer,
+ GParamSpec *pspec,
+ GspellInlineCheckerTextBuffer *spell)
+{
+ GspellChecker *new_checker;
+
+ new_checker = gspell_text_buffer_get_spell_checker (gspell_buffer);
+ set_spell_checker (spell, new_checker);
+
+ _gspell_current_word_policy_checker_changed (spell->current_word_policy);
+ recheck_all (spell);
+}
+
+static void
+set_buffer (GspellInlineCheckerTextBuffer *spell,
+ GtkTextBuffer *buffer)
+{
+ GtkTextTagTable *tag_table;
+ GtkTextIter start;
+ GspellTextBuffer *gspell_buffer;
+ GspellChecker *checker;
+ GdkRGBA underline_color;
+
+ g_return_if_fail (GTK_IS_TEXT_BUFFER (buffer));
+ g_return_if_fail (spell->buffer == NULL);
+ g_return_if_fail (spell->highlight_tag == NULL);
+ g_return_if_fail (spell->no_spell_check_tag == NULL);
+ g_return_if_fail (spell->mark_click == NULL);
+
+ spell->buffer = g_object_ref (buffer);
+
+ g_object_set_data (G_OBJECT (buffer),
+ INLINE_CHECKER_TEXT_BUFFER_KEY,
+ spell);
+
+ g_signal_connect_object (buffer,
+ "insert-text",
+ G_CALLBACK (insert_text_before_cb),
+ spell,
+ 0);
+
+ g_signal_connect_object (buffer,
+ "insert-text",
+ G_CALLBACK (insert_text_after_cb),
+ spell,
+ G_CONNECT_AFTER);
+
+ g_signal_connect_object (buffer,
+ "delete-range",
+ G_CALLBACK (delete_range_before_cb),
+ spell,
+ 0);
+
+ g_signal_connect_object (buffer,
+ "delete-range",
+ G_CALLBACK (delete_range_after_cb),
+ spell,
+ G_CONNECT_AFTER);
+
+ g_signal_connect_object (buffer,
+ "mark-set",
+ G_CALLBACK (mark_set_after_cb),
+ spell,
+ G_CONNECT_AFTER);
+
+ g_signal_connect_object (buffer,
+ "apply-tag",
+ G_CALLBACK (apply_or_remove_tag_cb),
+ spell,
+ G_CONNECT_AFTER);
+
+ g_signal_connect_object (buffer,
+ "remove-tag",
+ G_CALLBACK (apply_or_remove_tag_cb),
+ spell,
+ G_CONNECT_AFTER);
+
+ _gspell_utils_init_underline_rgba (&underline_color);
+
+ spell->highlight_tag = gtk_text_buffer_create_tag (spell->buffer, NULL,
+ "underline", PANGO_UNDERLINE_SINGLE,
+ "underline-rgba", &underline_color,
+ NULL);
+ g_object_ref (spell->highlight_tag);
+
+ spell->no_spell_check_tag = _gspell_utils_get_no_spell_check_tag (spell->buffer);
+ if (spell->no_spell_check_tag != NULL)
+ {
+ g_object_ref (spell->no_spell_check_tag);
+ }
+
+ tag_table = gtk_text_buffer_get_tag_table (spell->buffer);
+
+ g_signal_connect_object (tag_table,
+ "tag-added",
+ G_CALLBACK (tag_added_cb),
+ spell,
+ 0);
+
+ g_signal_connect_object (tag_table,
+ "tag-removed",
+ G_CALLBACK (tag_removed_cb),
+ spell,
+ 0);
+
+ /* For now we don't care where the mark points. The start looks like a
+ * good place to begin with.
+ */
+ gtk_text_buffer_get_start_iter (spell->buffer, &start);
+ spell->mark_click = gtk_text_buffer_create_mark (spell->buffer, NULL, &start, TRUE);
+
+ gspell_buffer = gspell_text_buffer_get_from_gtk_text_buffer (spell->buffer);
+ checker = gspell_text_buffer_get_spell_checker (gspell_buffer);
+ set_spell_checker (spell, checker);
+
+ g_signal_connect_object (gspell_buffer,
+ "notify::spell-checker",
+ G_CALLBACK (spell_checker_notify_cb),
+ spell,
+ 0);
+
+ recheck_all (spell);
+
+ g_object_notify (G_OBJECT (spell), "buffer");
+}
+
+static void
+_gspell_inline_checker_text_buffer_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GspellInlineCheckerTextBuffer *spell = GSPELL_INLINE_CHECKER_TEXT_BUFFER (object);
+
+ switch (prop_id)
+ {
+ case PROP_BUFFER:
+ g_value_set_object (value, spell->buffer);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+_gspell_inline_checker_text_buffer_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GspellInlineCheckerTextBuffer *spell = GSPELL_INLINE_CHECKER_TEXT_BUFFER (object);
+
+ switch (prop_id)
+ {
+ case PROP_BUFFER:
+ set_buffer (spell, g_value_get_object (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+_gspell_inline_checker_text_buffer_dispose (GObject *object)
+{
+ GspellInlineCheckerTextBuffer *spell = GSPELL_INLINE_CHECKER_TEXT_BUFFER (object);
+
+ if (spell->buffer != NULL)
+ {
+ GtkTextTagTable *table;
+
+ table = gtk_text_buffer_get_tag_table (spell->buffer);
+
+ if (table != NULL && spell->highlight_tag != NULL)
+ {
+ gtk_text_tag_table_remove (table, spell->highlight_tag);
+ }
+
+ if (spell->mark_click != NULL)
+ {
+ gtk_text_buffer_delete_mark (spell->buffer, spell->mark_click);
+ spell->mark_click = NULL;
+ }
+
+ g_object_set_data (G_OBJECT (spell->buffer), INLINE_CHECKER_TEXT_BUFFER_KEY, NULL);
+
+ g_object_unref (spell->buffer);
+ spell->buffer = NULL;
+ }
+
+ set_spell_checker (spell, NULL);
+
+ g_clear_object (&spell->highlight_tag);
+ g_clear_object (&spell->no_spell_check_tag);
+ g_clear_object (&spell->scan_region);
+ g_clear_object (&spell->current_word_policy);
+
+ g_slist_free (spell->views);
+ spell->views = NULL;
+
+ spell->mark_click = NULL;
+
+ if (spell->timeout_id != 0)
+ {
+ g_source_remove (spell->timeout_id);
+ spell->timeout_id = 0;
+ }
+
+ G_OBJECT_CLASS (_gspell_inline_checker_text_buffer_parent_class)->dispose (object);
+}
+
+static void
+_gspell_inline_checker_text_buffer_class_init (GspellInlineCheckerTextBufferClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->get_property = _gspell_inline_checker_text_buffer_get_property;
+ object_class->set_property = _gspell_inline_checker_text_buffer_set_property;
+ object_class->dispose = _gspell_inline_checker_text_buffer_dispose;
+
+ g_object_class_install_property (object_class,
+ PROP_BUFFER,
+ g_param_spec_object ("buffer",
+ "Buffer",
+ "",
+ GTK_TYPE_TEXT_BUFFER,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS));
+}
+
+static void
+_gspell_inline_checker_text_buffer_init (GspellInlineCheckerTextBuffer *spell)
+{
+ spell->current_word_policy = _gspell_current_word_policy_new ();
+}
+
+GspellInlineCheckerTextBuffer *
+_gspell_inline_checker_text_buffer_new (GtkTextBuffer *buffer)
+{
+ GspellInlineCheckerTextBuffer *spell;
+
+ g_return_val_if_fail (GTK_IS_TEXT_BUFFER (buffer), NULL);
+
+ spell = g_object_get_data (G_OBJECT (buffer), INLINE_CHECKER_TEXT_BUFFER_KEY);
+ if (spell != NULL)
+ {
+ return g_object_ref (spell);
+ }
+
+ return g_object_new (GSPELL_TYPE_INLINE_CHECKER_TEXT_BUFFER,
+ "buffer", buffer,
+ NULL);
+}
+
+/**
+ * _gspell_inline_checker_text_buffer_attach_view:
+ * @spell: a #GspellInlineCheckerTextBuffer.
+ * @view: a #GtkTextView.
+ *
+ * Enables the inline spell checker for @view. @view must have the same buffer as
+ * the #GspellInlineCheckerTextBuffer:buffer property.
+ */
+void
+_gspell_inline_checker_text_buffer_attach_view (GspellInlineCheckerTextBuffer *spell,
+ GtkTextView *view)
+{
+ g_return_if_fail (GSPELL_IS_INLINE_CHECKER_TEXT_BUFFER (spell));
+ g_return_if_fail (GTK_IS_TEXT_VIEW (view));
+ g_return_if_fail (gtk_text_view_get_buffer (view) == spell->buffer);
+ g_return_if_fail (g_slist_find (spell->views, view) == NULL);
+
+ g_signal_connect_object (view,
+ "button-press-event",
+ G_CALLBACK (button_press_event_cb),
+ spell,
+ 0);
+
+ g_signal_connect_object (view,
+ "popup-menu",
+ G_CALLBACK (popup_menu_cb),
+ spell,
+ 0);
+
+ g_signal_connect_object (view,
+ "draw",
+ G_CALLBACK (draw_cb),
+ spell,
+ 0);
+
+ spell->views = g_slist_prepend (spell->views, view);
+
+ _gspell_current_word_policy_set_check_current_word (spell->current_word_policy, TRUE);
+ check_visible_region_in_view (spell, view);
+}
+
+/**
+ * _gspell_inline_checker_text_buffer_detach_view:
+ * @spell: a #GspellInlineCheckerTextBuffer.
+ * @view: a #GtkTextView.
+ *
+ * Disables the inline spell checker for @view.
+ */
+void
+_gspell_inline_checker_text_buffer_detach_view (GspellInlineCheckerTextBuffer *spell,
+ GtkTextView *view)
+{
+ g_return_if_fail (GSPELL_IS_INLINE_CHECKER_TEXT_BUFFER (spell));
+ g_return_if_fail (GTK_IS_TEXT_VIEW (view));
+ g_return_if_fail (g_slist_find (spell->views, view) != NULL);
+
+ g_signal_handlers_disconnect_by_data (view, spell);
+
+ spell->views = g_slist_remove (spell->views, view);
+}
+
+void
+_gspell_inline_checker_text_buffer_set_unit_test_mode (GspellInlineCheckerTextBuffer *spell,
+ gboolean unit_test_mode)
+{
+ g_return_if_fail (GSPELL_IS_INLINE_CHECKER_TEXT_BUFFER (spell));
+
+ spell->unit_test_mode = unit_test_mode != FALSE;
+
+ if (spell->timeout_id != 0)
+ {
+ g_source_remove (spell->timeout_id);
+ spell->timeout_id = 0;
+ timeout_cb (spell);
+ }
+
+ check_visible_region (spell);
+}
+
+GtkTextTag *
+_gspell_inline_checker_text_buffer_get_highlight_tag (GspellInlineCheckerTextBuffer *spell)
+{
+ g_return_val_if_fail (GSPELL_IS_INLINE_CHECKER_TEXT_BUFFER (spell), NULL);
+
+ return spell->highlight_tag;
+}
+
+/* ex:set ts=8 noet: */
diff --git a/gspell/gspell-inline-checker-text-buffer.h b/gspell/gspell-inline-checker-text-buffer.h
new file mode 100644
index 0000000..988041b
--- /dev/null
+++ b/gspell/gspell-inline-checker-text-buffer.h
@@ -0,0 +1,69 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2002 - Paolo Maggi
+ * Copyright 2015 - Sébastien Wilmet
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+/* This is a modified version of GtkSpell 2.0.2 (gtkspell.sf.net)
+ * Copyright 2002 - Evan Martin
+ */
+
+#ifndef GSPELL_INLINE_CHECKER_TEXT_BUFFER_H
+#define GSPELL_INLINE_CHECKER_TEXT_BUFFER_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GSPELL_TYPE_INLINE_CHECKER_TEXT_BUFFER (_gspell_inline_checker_text_buffer_get_type ())
+
+G_GNUC_INTERNAL
+G_DECLARE_FINAL_TYPE (GspellInlineCheckerTextBuffer, _gspell_inline_checker_text_buffer,
+ GSPELL, INLINE_CHECKER_TEXT_BUFFER,
+ GObject)
+
+G_GNUC_INTERNAL
+GspellInlineCheckerTextBuffer *
+ _gspell_inline_checker_text_buffer_new (GtkTextBuffer *buffer);
+
+G_GNUC_INTERNAL
+void _gspell_inline_checker_text_buffer_attach_view (GspellInlineCheckerTextBuffer *spell,
+ GtkTextView *view);
+
+G_GNUC_INTERNAL
+void _gspell_inline_checker_text_buffer_detach_view (GspellInlineCheckerTextBuffer *spell,
+ GtkTextView *view);
+
+G_GNUC_INTERNAL
+void _gspell_inline_checker_text_buffer_populate_popup (GspellInlineCheckerTextBuffer *spell,
+ GtkMenu *menu);
+
+/* For unit tests */
+
+G_GNUC_INTERNAL
+void _gspell_inline_checker_text_buffer_set_unit_test_mode (GspellInlineCheckerTextBuffer *spell,
+ gboolean
unit_test_mode);
+
+G_GNUC_INTERNAL
+GtkTextTag *
+ _gspell_inline_checker_text_buffer_get_highlight_tag (GspellInlineCheckerTextBuffer *spell);
+
+G_END_DECLS
+
+#endif /* GSPELL_INLINE_CHECKER_TEXT_BUFFER_H */
+
+/* ex:set ts=8 noet: */
diff --git a/gspell/gspell-language-chooser-button.c b/gspell/gspell-language-chooser-button.c
new file mode 100644
index 0000000..72ba09e
--- /dev/null
+++ b/gspell/gspell-language-chooser-button.c
@@ -0,0 +1,328 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2015, 2016 - Sébastien Wilmet <swilmet gnome org>
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "gspell-language-chooser-button.h"
+#include <glib/gi18n-lib.h>
+#include "gspell-language-chooser.h"
+#include "gspell-language-chooser-dialog.h"
+
+/**
+ * SECTION:language-chooser-button
+ * @Short_description: Button to choose a GspellLanguage
+ * @Title: GspellLanguageChooserButton
+ * @See_also: #GspellLanguage, #GspellLanguageChooser
+ *
+ * #GspellLanguageChooserButton is a #GtkButton to choose an available
+ * #GspellLanguage. #GspellLanguageChooserButton implements the
+ * #GspellLanguageChooser interface.
+ *
+ * The button contains a label with the #GspellLanguageChooser:language name, as
+ * returned by gspell_language_get_name(). When the button is clicked, a
+ * #GspellLanguageChooserDialog is launched to choose the language.
+ */
+
+typedef struct _GspellLanguageChooserButtonPrivate GspellLanguageChooserButtonPrivate;
+
+struct _GspellLanguageChooserButtonPrivate
+{
+ GspellLanguageChooserDialog *dialog;
+ const GspellLanguage *language;
+ guint default_language : 1;
+};
+
+enum
+{
+ PROP_0,
+ PROP_LANGUAGE,
+ PROP_LANGUAGE_CODE,
+};
+
+static void gspell_language_chooser_button_iface_init (GspellLanguageChooserInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (GspellLanguageChooserButton,
+ gspell_language_chooser_button,
+ GTK_TYPE_BUTTON,
+ G_ADD_PRIVATE (GspellLanguageChooserButton)
+ G_IMPLEMENT_INTERFACE (GSPELL_TYPE_LANGUAGE_CHOOSER,
+ gspell_language_chooser_button_iface_init))
+
+static void
+update_button_label (GspellLanguageChooserButton *button)
+{
+ GspellLanguageChooserButtonPrivate *priv;
+
+ priv = gspell_language_chooser_button_get_instance_private (button);
+
+ if (priv->language != NULL)
+ {
+ gtk_button_set_label (GTK_BUTTON (button),
+ gspell_language_get_name (priv->language));
+ }
+ else
+ {
+ gtk_button_set_label (GTK_BUTTON (button),
+ _("No language selected"));
+ }
+}
+
+static const GspellLanguage *
+gspell_language_chooser_button_get_language_full (GspellLanguageChooser *chooser,
+ gboolean *default_language)
+{
+ GspellLanguageChooserButtonPrivate *priv;
+
+ priv = gspell_language_chooser_button_get_instance_private (GSPELL_LANGUAGE_CHOOSER_BUTTON (chooser));
+
+ if (default_language != NULL)
+ {
+ *default_language = priv->default_language;
+ }
+
+ return priv->language;
+}
+
+static void
+gspell_language_chooser_button_set_language (GspellLanguageChooser *chooser,
+ const GspellLanguage *language)
+{
+ GspellLanguageChooserButton *button;
+ GspellLanguageChooserButtonPrivate *priv;
+ gboolean default_language;
+ gboolean notify_language_code = FALSE;
+
+ button = GSPELL_LANGUAGE_CHOOSER_BUTTON (chooser);
+ priv = gspell_language_chooser_button_get_instance_private (button);
+
+ default_language = (language == NULL);
+
+ if (priv->default_language != default_language)
+ {
+ priv->default_language = default_language;
+ notify_language_code = TRUE;
+ }
+
+ if (language == NULL)
+ {
+ language = gspell_language_get_default ();
+ }
+
+ if (priv->language != language)
+ {
+ priv->language = language;
+
+ update_button_label (button);
+
+ g_object_notify (G_OBJECT (chooser), "language");
+ notify_language_code = TRUE;
+ }
+
+ if (notify_language_code)
+ {
+ g_object_notify (G_OBJECT (chooser), "language-code");
+ }
+}
+
+static void
+gspell_language_chooser_button_iface_init (GspellLanguageChooserInterface *iface)
+{
+ iface->get_language_full = gspell_language_chooser_button_get_language_full;
+ iface->set_language = gspell_language_chooser_button_set_language;
+}
+
+static void
+gspell_language_chooser_button_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GspellLanguageChooser *chooser = GSPELL_LANGUAGE_CHOOSER (object);
+
+ switch (prop_id)
+ {
+ case PROP_LANGUAGE:
+ g_value_set_boxed (value, gspell_language_chooser_get_language (chooser));
+ break;
+
+ case PROP_LANGUAGE_CODE:
+ g_value_set_string (value, gspell_language_chooser_get_language_code (chooser));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gspell_language_chooser_button_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GspellLanguageChooser *chooser = GSPELL_LANGUAGE_CHOOSER (object);
+
+ switch (prop_id)
+ {
+ case PROP_LANGUAGE:
+ gspell_language_chooser_set_language (chooser, g_value_get_boxed (value));
+ break;
+
+ case PROP_LANGUAGE_CODE:
+ gspell_language_chooser_set_language_code (chooser, g_value_get_string (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gspell_language_chooser_button_constructed (GObject *object)
+{
+ G_OBJECT_CLASS (gspell_language_chooser_button_parent_class)->constructed (object);
+
+ update_button_label (GSPELL_LANGUAGE_CHOOSER_BUTTON (object));
+}
+
+static void
+dialog_response_cb (GtkDialog *dialog,
+ gint response)
+{
+ gtk_widget_destroy (GTK_WIDGET (dialog));
+}
+
+static void
+dialog_destroy_cb (GtkWidget *dialog,
+ GspellLanguageChooserButton *button)
+{
+ GspellLanguageChooserButtonPrivate *priv;
+
+ priv = gspell_language_chooser_button_get_instance_private (button);
+
+ priv->dialog = NULL;
+}
+
+static void
+ensure_dialog (GspellLanguageChooserButton *button)
+{
+ GspellLanguageChooserButtonPrivate *priv;
+ GtkWidget *toplevel;
+ GtkWindow *parent = NULL;
+
+ priv = gspell_language_chooser_button_get_instance_private (button);
+
+ if (priv->dialog != NULL)
+ {
+ return;
+ }
+
+ toplevel = gtk_widget_get_toplevel (GTK_WIDGET (button));
+ if (gtk_widget_is_toplevel (toplevel) && GTK_IS_WINDOW (toplevel))
+ {
+ parent = GTK_WINDOW (toplevel);
+ }
+
+ priv->dialog = GSPELL_LANGUAGE_CHOOSER_DIALOG (
+ gspell_language_chooser_dialog_new (parent,
+ priv->default_language ? NULL : priv->language,
+ GTK_DIALOG_DESTROY_WITH_PARENT |
+ GTK_DIALOG_USE_HEADER_BAR));
+
+ if (parent != NULL)
+ {
+ gtk_window_set_modal (GTK_WINDOW (priv->dialog),
+ gtk_window_get_modal (parent));
+ }
+
+ g_object_bind_property (priv->dialog, "language-code",
+ button, "language-code",
+ G_BINDING_DEFAULT);
+
+ g_signal_connect (priv->dialog,
+ "response",
+ G_CALLBACK (dialog_response_cb),
+ NULL);
+
+ g_signal_connect_object (priv->dialog,
+ "destroy",
+ G_CALLBACK (dialog_destroy_cb),
+ button,
+ 0);
+}
+
+static void
+gspell_language_chooser_button_clicked (GtkButton *gtk_button)
+{
+ GspellLanguageChooserButton *button;
+ GspellLanguageChooserButtonPrivate *priv;
+
+ button = GSPELL_LANGUAGE_CHOOSER_BUTTON (gtk_button);
+ priv = gspell_language_chooser_button_get_instance_private (button);
+
+ /* If the dialog isn't modal, the button can be clicked several times. */
+ ensure_dialog (button);
+
+ gspell_language_chooser_set_language (GSPELL_LANGUAGE_CHOOSER (priv->dialog),
+ priv->default_language ? NULL : priv->language);
+
+ gtk_window_present (GTK_WINDOW (priv->dialog));
+}
+
+static void
+gspell_language_chooser_button_class_init (GspellLanguageChooserButtonClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkButtonClass *button_class = GTK_BUTTON_CLASS (klass);
+
+ object_class->get_property = gspell_language_chooser_button_get_property;
+ object_class->set_property = gspell_language_chooser_button_set_property;
+ object_class->constructed = gspell_language_chooser_button_constructed;
+
+ button_class->clicked = gspell_language_chooser_button_clicked;
+
+ g_object_class_override_property (object_class, PROP_LANGUAGE, "language");
+ g_object_class_override_property (object_class, PROP_LANGUAGE_CODE, "language-code");
+}
+
+static void
+gspell_language_chooser_button_init (GspellLanguageChooserButton *button)
+{
+}
+
+/**
+ * gspell_language_chooser_button_new:
+ * @current_language: (nullable): a #GspellLanguage, or %NULL to pick the
+ * default language.
+ *
+ * Returns: a new #GspellLanguageChooserButton widget.
+ */
+GtkWidget *
+gspell_language_chooser_button_new (const GspellLanguage *current_language)
+{
+ return g_object_new (GSPELL_TYPE_LANGUAGE_CHOOSER_BUTTON,
+ "language", current_language,
+ NULL);
+}
+
+/* ex:set ts=8 noet: */
diff --git a/gspell/gspell-language-chooser-button.h b/gspell/gspell-language-chooser-button.h
new file mode 100644
index 0000000..5dd9204
--- /dev/null
+++ b/gspell/gspell-language-chooser-button.h
@@ -0,0 +1,55 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2015 - Sébastien Wilmet <swilmet gnome org>
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GSPELL_LANGUAGE_CHOOSER_BUTTON_H
+#define GSPELL_LANGUAGE_CHOOSER_BUTTON_H
+
+#if !defined (GSPELL_H_INSIDE) && !defined (GSPELL_COMPILATION)
+#error "Only <gspell/gspell.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+#include <gspell/gspell-language.h>
+#include <gspell/gspell-version.h>
+
+G_BEGIN_DECLS
+
+#define GSPELL_TYPE_LANGUAGE_CHOOSER_BUTTON (gspell_language_chooser_button_get_type ())
+
+GSPELL_AVAILABLE_IN_ALL
+G_DECLARE_DERIVABLE_TYPE (GspellLanguageChooserButton, gspell_language_chooser_button,
+ GSPELL, LANGUAGE_CHOOSER_BUTTON,
+ GtkButton)
+
+struct _GspellLanguageChooserButtonClass
+{
+ GtkButtonClass parent_class;
+
+ /* Padding for future expansion */
+ gpointer padding[8];
+};
+
+GSPELL_AVAILABLE_IN_ALL
+GtkWidget * gspell_language_chooser_button_new (const GspellLanguage *current_language);
+
+G_END_DECLS
+
+#endif /* GSPELL_LANGUAGE_CHOOSER_BUTTON_H */
+
+/* ex:set ts=8 noet: */
diff --git a/gspell/gspell-language-chooser-dialog.c b/gspell/gspell-language-chooser-dialog.c
new file mode 100644
index 0000000..beac697
--- /dev/null
+++ b/gspell/gspell-language-chooser-dialog.c
@@ -0,0 +1,498 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2002 - Paolo Maggi
+ * Copyright 2015, 2016 - Sébastien Wilmet
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "gspell-language-chooser-dialog.h"
+#include "gspell-language-chooser.h"
+
+/**
+ * SECTION:language-chooser-dialog
+ * @Short_description: Dialog to choose a GspellLanguage
+ * @Title: GspellLanguageChooserDialog
+ * @See_also: #GspellLanguage, #GspellLanguageChooser
+ *
+ * #GspellLanguageChooserDialog is a #GtkDialog to choose an available
+ * #GspellLanguage. #GspellLanguageChooserDialog implements the
+ * #GspellLanguageChooser interface.
+ *
+ * The #GspellLanguageChooser:language and #GspellLanguageChooser:language-code
+ * properties are updated only when the Select button is pressed or when a row
+ * is activated (e.g. with a double-click).
+ *
+ * The application is responsible to destroy the dialog, typically when the
+ * #GtkDialog::response signal has been received or gtk_dialog_run() has
+ * returned.
+ */
+
+typedef struct _GspellLanguageChooserDialogPrivate GspellLanguageChooserDialogPrivate;
+
+struct _GspellLanguageChooserDialogPrivate
+{
+ GtkTreeView *treeview;
+ const GspellLanguage *language;
+ guint default_language : 1;
+};
+
+enum
+{
+ PROP_0,
+ PROP_LANGUAGE,
+ PROP_LANGUAGE_CODE,
+};
+
+enum
+{
+ COLUMN_LANGUAGE_NAME,
+ COLUMN_LANGUAGE_POINTER,
+ N_COLUMNS
+};
+
+static void gspell_language_chooser_dialog_iface_init (GspellLanguageChooserInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (GspellLanguageChooserDialog,
+ gspell_language_chooser_dialog,
+ GTK_TYPE_DIALOG,
+ G_ADD_PRIVATE (GspellLanguageChooserDialog)
+ G_IMPLEMENT_INTERFACE (GSPELL_TYPE_LANGUAGE_CHOOSER,
+ gspell_language_chooser_dialog_iface_init))
+
+static void
+scroll_to_selected (GtkTreeView *tree_view)
+{
+ GtkTreeModel *model;
+ GtkTreeSelection *selection;
+ GtkTreeIter iter;
+
+ model = gtk_tree_view_get_model (tree_view);
+ g_return_if_fail (model != NULL);
+
+ selection = gtk_tree_view_get_selection (tree_view);
+
+ if (gtk_tree_selection_get_selected (selection, NULL, &iter))
+ {
+ GtkTreePath *path;
+
+ path = gtk_tree_model_get_path (model, &iter);
+ g_return_if_fail (path != NULL);
+
+ gtk_tree_view_scroll_to_cell (tree_view, path, NULL, TRUE, 1.0, 0.0);
+ gtk_tree_path_free (path);
+ }
+}
+
+static const GspellLanguage *
+gspell_language_chooser_dialog_get_language_full (GspellLanguageChooser *chooser,
+ gboolean *default_language)
+{
+ GspellLanguageChooserDialog *dialog;
+ GspellLanguageChooserDialogPrivate *priv;
+
+ dialog = GSPELL_LANGUAGE_CHOOSER_DIALOG (chooser);
+ priv = gspell_language_chooser_dialog_get_instance_private (dialog);
+
+ if (default_language != NULL)
+ {
+ *default_language = priv->default_language;
+ }
+
+ return priv->language;
+}
+
+static void
+gspell_language_chooser_dialog_set_language (GspellLanguageChooser *chooser,
+ const GspellLanguage *language_param)
+{
+ GspellLanguageChooserDialog *dialog;
+ GspellLanguageChooserDialogPrivate *priv;
+ const GspellLanguage *language;
+ GtkTreeSelection *selection;
+ GtkTreeModel *model;
+ GtkTreeIter iter;
+
+ dialog = GSPELL_LANGUAGE_CHOOSER_DIALOG (chooser);
+ priv = gspell_language_chooser_dialog_get_instance_private (dialog);
+
+ language = language_param;
+
+ if (language == NULL)
+ {
+ language = gspell_language_get_default ();
+ }
+
+ selection = gtk_tree_view_get_selection (priv->treeview);
+
+ if (language == NULL)
+ {
+ gboolean notify_language_code = FALSE;
+
+ gtk_tree_selection_unselect_all (selection);
+
+ /* Update first the full state before notifying the properties. */
+ if (!priv->default_language)
+ {
+ priv->default_language = TRUE;
+ notify_language_code = TRUE;
+ }
+
+ if (priv->language != NULL)
+ {
+ priv->language = NULL;
+ g_object_notify (G_OBJECT (dialog), "language");
+ }
+
+ if (notify_language_code)
+ {
+ g_object_notify (G_OBJECT (dialog), "language-code");
+ }
+
+ return;
+ }
+
+ model = gtk_tree_view_get_model (priv->treeview);
+
+ if (!gtk_tree_model_get_iter_first (model, &iter))
+ {
+ goto warning;
+ }
+
+ do
+ {
+ const GspellLanguage *cur_lang;
+
+ gtk_tree_model_get (model, &iter,
+ COLUMN_LANGUAGE_POINTER, &cur_lang,
+ -1);
+
+ if (language == cur_lang)
+ {
+ gboolean default_language;
+ gboolean notify_language_code = FALSE;
+
+ gtk_tree_selection_select_iter (selection, &iter);
+ scroll_to_selected (priv->treeview);
+
+ /* Update first the full state before notifying the properties. */
+
+ default_language = language_param == NULL;
+
+ if (priv->default_language != default_language)
+ {
+ priv->default_language = default_language;
+ notify_language_code = TRUE;
+ }
+
+ if (priv->language != language)
+ {
+ priv->language = language;
+ g_object_notify (G_OBJECT (dialog), "language");
+ notify_language_code = TRUE;
+ }
+
+ if (notify_language_code)
+ {
+ g_object_notify (G_OBJECT (dialog), "language-code");
+ }
+
+ return;
+ }
+ }
+ while (gtk_tree_model_iter_next (model, &iter));
+
+warning:
+ g_warning ("GspellLanguageChooserDialog: setting language failed, language not found.");
+}
+
+static void
+gspell_language_chooser_dialog_iface_init (GspellLanguageChooserInterface *iface)
+{
+ iface->get_language_full = gspell_language_chooser_dialog_get_language_full;
+ iface->set_language = gspell_language_chooser_dialog_set_language;
+}
+
+static void
+gspell_language_chooser_dialog_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GspellLanguageChooser *chooser = GSPELL_LANGUAGE_CHOOSER (object);
+
+ switch (prop_id)
+ {
+ case PROP_LANGUAGE:
+ g_value_set_boxed (value, gspell_language_chooser_get_language (chooser));
+ break;
+
+ case PROP_LANGUAGE_CODE:
+ g_value_set_string (value, gspell_language_chooser_get_language_code (chooser));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gspell_language_chooser_dialog_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GspellLanguageChooser *chooser = GSPELL_LANGUAGE_CHOOSER (object);
+
+ switch (prop_id)
+ {
+ case PROP_LANGUAGE:
+ gspell_language_chooser_set_language (chooser, g_value_get_boxed (value));
+ break;
+
+ case PROP_LANGUAGE_CODE:
+ gspell_language_chooser_set_language_code (chooser, g_value_get_string (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gspell_language_chooser_dialog_constructed (GObject *object)
+{
+ gint use_header_bar;
+
+ if (G_OBJECT_CLASS (gspell_language_chooser_dialog_parent_class)->constructed != NULL)
+ {
+ G_OBJECT_CLASS (gspell_language_chooser_dialog_parent_class)->constructed (object);
+ }
+
+ g_object_get (object,
+ "use-header-bar", &use_header_bar,
+ NULL);
+
+ if (use_header_bar)
+ {
+ /* Avoid the title being ellipsized, if possible (for translations too). */
+ gtk_widget_set_size_request (GTK_WIDGET (object), 450, -1);
+ }
+}
+
+static void
+dialog_response_cb (GtkDialog *gtk_dialog,
+ gint response)
+{
+ GspellLanguageChooserDialog *dialog;
+ GspellLanguageChooserDialogPrivate *priv;
+ GtkTreeSelection *selection;
+ GtkTreeModel *model;
+ GtkTreeIter iter;
+ const GspellLanguage *lang;
+ gboolean notify_language_code = FALSE;
+
+ if (response != GTK_RESPONSE_OK)
+ {
+ return;
+ }
+
+ dialog = GSPELL_LANGUAGE_CHOOSER_DIALOG (gtk_dialog);
+ priv = gspell_language_chooser_dialog_get_instance_private (dialog);
+
+ selection = gtk_tree_view_get_selection (priv->treeview);
+
+ if (!gtk_tree_selection_get_selected (selection, &model, &iter))
+ {
+ return;
+ }
+
+ gtk_tree_model_get (model, &iter,
+ COLUMN_LANGUAGE_POINTER, &lang,
+ -1);
+
+ /* Update first the full state before notifying the properties. */
+
+ if (priv->default_language)
+ {
+ priv->default_language = FALSE;
+ notify_language_code = TRUE;
+ }
+
+ if (priv->language != lang)
+ {
+ priv->language = lang;
+ g_object_notify (G_OBJECT (dialog), "language");
+ notify_language_code = TRUE;
+ }
+
+ if (notify_language_code)
+ {
+ g_object_notify (G_OBJECT (dialog), "language-code");
+ }
+}
+
+static void
+gspell_language_chooser_dialog_class_init (GspellLanguageChooserDialogClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->get_property = gspell_language_chooser_dialog_get_property;
+ object_class->set_property = gspell_language_chooser_dialog_set_property;
+ object_class->constructed = gspell_language_chooser_dialog_constructed;
+
+ g_object_class_override_property (object_class, PROP_LANGUAGE, "language");
+ g_object_class_override_property (object_class, PROP_LANGUAGE_CODE, "language-code");
+
+ /* Bind class to template */
+ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/gspell/language-dialog.ui");
+ gtk_widget_class_bind_template_child_private (widget_class, GspellLanguageChooserDialog, treeview);
+}
+
+static void
+row_activated_cb (GtkTreeView *tree_view,
+ GtkTreePath *path,
+ GtkTreeViewColumn *column,
+ GspellLanguageChooserDialog *dialog)
+{
+ gtk_dialog_response (GTK_DIALOG (dialog), GTK_RESPONSE_OK);
+}
+
+static void
+populate_language_list (GspellLanguageChooserDialog *dialog)
+{
+ GspellLanguageChooserDialogPrivate *priv;
+ GtkListStore *store;
+ const GList *available_langs;
+ const GList *l;
+
+ priv = gspell_language_chooser_dialog_get_instance_private (dialog);
+
+ store = GTK_LIST_STORE (gtk_tree_view_get_model (priv->treeview));
+
+ available_langs = gspell_language_get_available ();
+
+ for (l = available_langs; l != NULL; l = l->next)
+ {
+ const GspellLanguage *lang = l->data;
+ const gchar *name;
+ GtkTreeIter iter;
+
+ name = gspell_language_get_name (lang);
+
+ gtk_list_store_append (store, &iter);
+ gtk_list_store_set (store, &iter,
+ COLUMN_LANGUAGE_NAME, name,
+ COLUMN_LANGUAGE_POINTER, lang,
+ -1);
+ }
+}
+
+static void
+gspell_language_chooser_dialog_init (GspellLanguageChooserDialog *dialog)
+{
+ GspellLanguageChooserDialogPrivate *priv;
+ GtkListStore *store;
+ GtkTreeSelection *selection;
+ GtkTreeViewColumn *column;
+ GtkCellRenderer *renderer;
+
+ priv = gspell_language_chooser_dialog_get_instance_private (dialog);
+
+ priv->default_language = TRUE;
+
+ gtk_widget_init_template (GTK_WIDGET (dialog));
+
+ store = gtk_list_store_new (N_COLUMNS, G_TYPE_STRING, GSPELL_TYPE_LANGUAGE);
+ gtk_tree_view_set_model (priv->treeview, GTK_TREE_MODEL (store));
+ g_object_unref (store);
+
+ selection = gtk_tree_view_get_selection (priv->treeview);
+ gtk_tree_selection_set_mode (selection, GTK_SELECTION_BROWSE);
+
+ /* Add the language column */
+ column = gtk_tree_view_column_new ();
+ renderer = gtk_cell_renderer_text_new ();
+ gtk_tree_view_column_pack_start (column, renderer, TRUE);
+ gtk_tree_view_column_add_attribute (column, renderer,
+ "text", COLUMN_LANGUAGE_NAME);
+
+ gtk_tree_view_append_column (priv->treeview, column);
+
+ gtk_tree_view_set_search_column (priv->treeview, COLUMN_LANGUAGE_NAME);
+
+ gtk_widget_grab_focus (GTK_WIDGET (priv->treeview));
+
+ populate_language_list (dialog);
+
+ g_signal_connect (priv->treeview,
+ "realize",
+ G_CALLBACK (scroll_to_selected),
+ dialog);
+
+ g_signal_connect (priv->treeview,
+ "row-activated",
+ G_CALLBACK (row_activated_cb),
+ dialog);
+
+ /* Be the first to receive the signal, to notify the property before the
+ * dialog gets destroyed by the app.
+ * It means that the behavior of the dialog is not fully overridable by
+ * an app, but I don't think it's really important and worst case a new
+ * GspellLanguageChooser implementation can be written. If our
+ * ::response handler was done in the object method handler, apps would
+ * need to connect to the ::response signal with the after flag to
+ * destroy the dialog, which is less convenient and needs more
+ * documentation.
+ */
+ g_signal_connect (dialog,
+ "response",
+ G_CALLBACK (dialog_response_cb),
+ NULL);
+}
+
+/**
+ * gspell_language_chooser_dialog_new:
+ * @parent: transient parent of the dialog.
+ * @current_language: (nullable): the #GspellLanguage to select initially, or
+ * %NULL to pick the default language.
+ * @flags: #GtkDialogFlags
+ *
+ * Returns: a new #GspellLanguageChooserDialog widget.
+ */
+GtkWidget *
+gspell_language_chooser_dialog_new (GtkWindow *parent,
+ const GspellLanguage *current_language,
+ GtkDialogFlags flags)
+{
+ g_return_val_if_fail (GTK_IS_WINDOW (parent), NULL);
+
+ return g_object_new (GSPELL_TYPE_LANGUAGE_CHOOSER_DIALOG,
+ "transient-for", parent,
+ "language", current_language,
+ "modal", (flags & GTK_DIALOG_MODAL) != 0,
+ "destroy-with-parent", (flags & GTK_DIALOG_DESTROY_WITH_PARENT) != 0,
+ "use-header-bar", (flags & GTK_DIALOG_USE_HEADER_BAR) != 0,
+ NULL);
+}
+
+/* ex:set ts=8 noet: */
diff --git a/gspell/gspell-language-chooser-dialog.h b/gspell/gspell-language-chooser-dialog.h
new file mode 100644
index 0000000..39278b6
--- /dev/null
+++ b/gspell/gspell-language-chooser-dialog.h
@@ -0,0 +1,58 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2002 - Paolo Maggi
+ * Copyright 2015 - Sébastien Wilmet
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GSPELL_LANGUAGE_CHOOSER_DIALOG_H
+#define GSPELL_LANGUAGE_CHOOSER_DIALOG_H
+
+#if !defined (GSPELL_H_INSIDE) && !defined (GSPELL_COMPILATION)
+#error "Only <gspell/gspell.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+#include <gspell/gspell-language.h>
+#include <gspell/gspell-version.h>
+
+G_BEGIN_DECLS
+
+#define GSPELL_TYPE_LANGUAGE_CHOOSER_DIALOG (gspell_language_chooser_dialog_get_type ())
+
+GSPELL_AVAILABLE_IN_ALL
+G_DECLARE_DERIVABLE_TYPE (GspellLanguageChooserDialog, gspell_language_chooser_dialog,
+ GSPELL, LANGUAGE_CHOOSER_DIALOG,
+ GtkDialog)
+
+struct _GspellLanguageChooserDialogClass
+{
+ GtkDialogClass parent_class;
+
+ /* Padding for future expansion */
+ gpointer padding[8];
+};
+
+GSPELL_AVAILABLE_IN_ALL
+GtkWidget * gspell_language_chooser_dialog_new (GtkWindow *parent,
+ const GspellLanguage *current_language,
+ GtkDialogFlags flags);
+
+G_END_DECLS
+
+#endif /* GSPELL_LANGUAGE_CHOOSER_DIALOG_H */
+
+/* ex:set ts=8 noet: */
diff --git a/gspell/gspell-language-chooser.c b/gspell/gspell-language-chooser.c
new file mode 100644
index 0000000..164e6fd
--- /dev/null
+++ b/gspell/gspell-language-chooser.c
@@ -0,0 +1,177 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2015, 2016 - Sébastien Wilmet <swilmet gnome org>
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "gspell-language-chooser.h"
+
+/**
+ * SECTION:language-chooser
+ * @Short_description: Interface to choose a GspellLanguage
+ * @Title: GspellLanguageChooser
+ * @See_also: #GspellLanguage, #GspellLanguageChooserButton, #GspellLanguageChooserDialog
+ *
+ * #GspellLanguageChooser is an interface that is implemented by widgets for
+ * choosing a #GspellLanguage.
+ *
+ * There are two properties: #GspellLanguageChooser:language and
+ * #GspellLanguageChooser:language-code. They are kept in sync. The former is
+ * useful, for example, to bind it to the #GspellChecker's language property
+ * with g_object_bind_property(). The latter is useful to bind it to a
+ * #GSettings key with g_settings_bind().
+ *
+ * When setting the language, %NULL or the empty string can be passed to pick
+ * the default language. In that case, the #GspellLanguageChooser:language-code
+ * property will contain the empty string, whereas the
+ * #GspellLanguageChooser:language property will contain the actual
+ * #GspellLanguage as returned by gspell_language_get_default(). If the user
+ * launches the #GspellLanguageChooser and chooses explicitly a language, then
+ * the #GspellLanguageChooser:language-code property will no longer be empty,
+ * even if it is the same language as the default language.
+ *
+ * Note that if an explicit language (non-%NULL or not the empty string) is set
+ * to the #GspellLanguageChooser, then the #GspellLanguageChooser:language-code
+ * property will not be empty, it will contain the language code of the passed
+ * language, even if the language is the same as the default language.
+ *
+ * Thus, a good default value for a #GSettings key is the empty string. That
+ * way, the default language is picked, and can change depending on the locale.
+ * But once the user has chosen a language, that language is kept in the
+ * #GSettings key.
+ */
+
+G_DEFINE_INTERFACE (GspellLanguageChooser, gspell_language_chooser, G_TYPE_OBJECT)
+
+static void
+gspell_language_chooser_default_init (GspellLanguageChooserInterface *interface)
+{
+ /**
+ * GspellLanguageChooser:language:
+ *
+ * The selected #GspellLanguage.
+ */
+ g_object_interface_install_property (interface,
+ g_param_spec_boxed ("language",
+ "Language",
+ "",
+ GSPELL_TYPE_LANGUAGE,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * GspellLanguageChooser:language-code:
+ *
+ * The empty string if the default language was set and the selection
+ * hasn't changed. Or the language code if an explicit language was set
+ * or if the selection has changed.
+ */
+ g_object_interface_install_property (interface,
+ g_param_spec_string ("language-code",
+ "Language Code",
+ "",
+ "",
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+}
+
+/**
+ * gspell_language_chooser_get_language:
+ * @chooser: a #GspellLanguageChooser.
+ *
+ * Returns: (nullable): the selected #GspellLanguage, or %NULL if no
+ * dictionaries are available.
+ */
+const GspellLanguage *
+gspell_language_chooser_get_language (GspellLanguageChooser *chooser)
+{
+ g_return_val_if_fail (GSPELL_IS_LANGUAGE_CHOOSER (chooser), NULL);
+
+ return GSPELL_LANGUAGE_CHOOSER_GET_IFACE (chooser)->get_language_full (chooser, NULL);
+}
+
+/**
+ * gspell_language_chooser_set_language:
+ * @chooser: a #GspellLanguageChooser.
+ * @language: (nullable): a #GspellLanguage or %NULL to pick the default
+ * language.
+ *
+ * Sets the selected language.
+ */
+void
+gspell_language_chooser_set_language (GspellLanguageChooser *chooser,
+ const GspellLanguage *language)
+{
+ g_return_if_fail (GSPELL_IS_LANGUAGE_CHOOSER (chooser));
+
+ GSPELL_LANGUAGE_CHOOSER_GET_IFACE (chooser)->set_language (chooser, language);
+}
+
+/**
+ * gspell_language_chooser_get_language_code:
+ * @chooser: a #GspellLanguageChooser.
+ *
+ * Returns: the #GspellLanguageChooser:language-code. It cannot be %NULL.
+ */
+const gchar *
+gspell_language_chooser_get_language_code (GspellLanguageChooser *chooser)
+{
+ const GspellLanguage *lang;
+ gboolean default_lang = TRUE;
+ const gchar *language_code;
+
+ g_return_val_if_fail (GSPELL_IS_LANGUAGE_CHOOSER (chooser), "");
+
+ lang = GSPELL_LANGUAGE_CHOOSER_GET_IFACE (chooser)->get_language_full (chooser, &default_lang);
+
+ if (default_lang || lang == NULL)
+ {
+ return "";
+ }
+
+ language_code = gspell_language_get_code (lang);
+ g_return_val_if_fail (language_code != NULL, "");
+
+ return language_code;
+}
+
+/**
+ * gspell_language_chooser_set_language_code:
+ * @chooser: a #GspellLanguageChooser.
+ * @language_code: (nullable): a language code, or the empty string or %NULL to
+ * pick the default language.
+ */
+void
+gspell_language_chooser_set_language_code (GspellLanguageChooser *chooser,
+ const gchar *language_code)
+{
+ const GspellLanguage *lang = NULL;
+
+ g_return_if_fail (GSPELL_IS_LANGUAGE_CHOOSER (chooser));
+
+ if (language_code != NULL && language_code[0] != '\0')
+ {
+ lang = gspell_language_lookup (language_code);
+ }
+
+ GSPELL_LANGUAGE_CHOOSER_GET_IFACE (chooser)->set_language (chooser, lang);
+}
+
+/* ex:set ts=8 noet: */
diff --git a/gspell/gspell-language-chooser.h b/gspell/gspell-language-chooser.h
new file mode 100644
index 0000000..ed24fd5
--- /dev/null
+++ b/gspell/gspell-language-chooser.h
@@ -0,0 +1,72 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2015 - Sébastien Wilmet <swilmet gnome org>
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GSPELL_LANGUAGE_CHOOSER_H
+#define GSPELL_LANGUAGE_CHOOSER_H
+
+#if !defined (GSPELL_H_INSIDE) && !defined (GSPELL_COMPILATION)
+#error "Only <gspell/gspell.h> can be included directly."
+#endif
+
+#include <glib-object.h>
+#include <gspell/gspell-language.h>
+#include <gspell/gspell-version.h>
+
+G_BEGIN_DECLS
+
+#define GSPELL_TYPE_LANGUAGE_CHOOSER (gspell_language_chooser_get_type ())
+
+GSPELL_AVAILABLE_IN_ALL
+G_DECLARE_INTERFACE (GspellLanguageChooser, gspell_language_chooser,
+ GSPELL, LANGUAGE_CHOOSER,
+ GObject)
+
+struct _GspellLanguageChooserInterface
+{
+ GTypeInterface parent_interface;
+
+ /* The return value is the same as get_language(), but there is the
+ * (optional) out parameter @default_language in addition.
+ */
+ const GspellLanguage * (* get_language_full) (GspellLanguageChooser *chooser,
+ gboolean *default_language);
+
+ void (* set_language) (GspellLanguageChooser *chooser,
+ const GspellLanguage *language);
+};
+
+GSPELL_AVAILABLE_IN_ALL
+const GspellLanguage * gspell_language_chooser_get_language (GspellLanguageChooser *chooser);
+
+GSPELL_AVAILABLE_IN_ALL
+void gspell_language_chooser_set_language (GspellLanguageChooser *chooser,
+ const GspellLanguage *language);
+
+GSPELL_AVAILABLE_IN_ALL
+const gchar * gspell_language_chooser_get_language_code (GspellLanguageChooser *chooser);
+
+GSPELL_AVAILABLE_IN_ALL
+void gspell_language_chooser_set_language_code (GspellLanguageChooser *chooser,
+ const gchar
*language_code);
+
+G_END_DECLS
+
+#endif /* GSPELL_LANGUAGE_CHOOSER_H */
+
+/* ex:set ts=8 noet: */
diff --git a/gspell/gspell-language.c b/gspell/gspell-language.c
new file mode 100644
index 0000000..c1274f4
--- /dev/null
+++ b/gspell/gspell-language.c
@@ -0,0 +1,312 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2006 - Paolo Maggi
+ * Copyright 2008 - Novell, Inc.
+ * Copyright 2015, 2016, 2020 - Sébastien Wilmet
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "gspell-language.h"
+#include <string.h>
+#include <glib/gi18n-lib.h>
+#include <enchant.h>
+#include "gspell-icu.h"
+
+#ifdef OS_OSX
+#include "gspell-osx.h"
+#endif
+
+/**
+ * SECTION:language
+ * @Short_description: Language
+ * @Title: GspellLanguage
+ * @See_also: #GspellChecker
+ *
+ * #GspellLanguage represents a language that can be used for the spell
+ * checking, i.e. a language for which a dictionary is installed.
+ */
+
+struct _GspellLanguage
+{
+ gchar *code;
+ gchar *name;
+ gchar *collate_key;
+};
+
+G_DEFINE_BOXED_TYPE (GspellLanguage,
+ gspell_language,
+ gspell_language_copy,
+ gspell_language_free)
+
+static void
+spell_language_dict_describe_cb (const gchar * const language_code,
+ const gchar * const provider_name,
+ const gchar * const provider_desc,
+ const gchar * const provider_file,
+ gpointer user_data)
+{
+ GList **available_languages = user_data;
+ GList *l;
+ GspellLanguage *language;
+
+ g_return_if_fail (language_code != NULL);
+
+ for (l = *available_languages; l != NULL; l = l->next)
+ {
+ GspellLanguage *cur_language = l->data;
+
+ if (g_strcmp0 (cur_language->code, language_code) == 0)
+ {
+ /* Avoid duplicates. */
+ return;
+ }
+ }
+
+ language = g_slice_new (GspellLanguage);
+ language->code = g_strdup (language_code);
+
+ language->name = _gspell_icu_get_language_name_from_code (language_code, NULL);
+ if (language->name == NULL)
+ {
+ /* Translators: %s is the language ISO code. */
+ language->name = g_strdup_printf (C_("language", "Unknown (%s)"), language_code);
+ }
+
+ language->collate_key = g_utf8_collate_key (language->name, -1);
+
+ *available_languages = g_list_prepend (*available_languages, language);
+}
+
+/**
+ * gspell_language_get_available:
+ *
+ * Returns: (transfer none) (element-type GspellLanguage): the list of available
+ * languages, sorted with gspell_language_compare().
+ */
+const GList *
+gspell_language_get_available (void)
+{
+ static gboolean initialized = FALSE;
+ static GList *available_languages = NULL;
+ EnchantBroker *broker;
+
+ if (initialized)
+ {
+ return available_languages;
+ }
+
+ initialized = TRUE;
+
+ broker = enchant_broker_init ();
+ enchant_broker_list_dicts (broker,
+ spell_language_dict_describe_cb,
+ &available_languages);
+ enchant_broker_free (broker);
+
+ available_languages = g_list_sort (available_languages,
+ (GCompareFunc) gspell_language_compare);
+
+ return available_languages;
+}
+
+/**
+ * gspell_language_get_default:
+ *
+ * Finds the best available language based on the current locale.
+ *
+ * Returns: (nullable): the default #GspellLanguage, or %NULL if no dictionaries
+ * are available.
+ */
+const GspellLanguage *
+gspell_language_get_default (void)
+{
+ const GspellLanguage *lang;
+ const gchar * const *lang_names;
+ const GList *langs;
+ gint i;
+
+ /* Try with the current locale */
+ lang_names = g_get_language_names ();
+
+ for (i = 0; lang_names[i] != NULL; i++)
+ {
+ lang = gspell_language_lookup (lang_names[i]);
+
+ if (lang != NULL)
+ {
+ return lang;
+ }
+ }
+
+ /* Another try specific to Mac OS X */
+#ifdef OS_OSX
+ {
+ gchar *code = _gspell_osx_get_preferred_spell_language ();
+
+ if (code != NULL)
+ {
+ lang = gspell_language_lookup (code);
+ g_free (code);
+
+ if (lang != NULL)
+ {
+ return lang;
+ }
+ }
+ }
+#endif
+
+ /* Try English */
+ lang = gspell_language_lookup ("en_US");
+ if (lang != NULL)
+ {
+ return lang;
+ }
+
+ /* Take the first available language */
+ langs = gspell_language_get_available ();
+ if (langs != NULL)
+ {
+ return langs->data;
+ }
+
+ return NULL;
+}
+
+/**
+ * gspell_language_lookup:
+ * @language_code: a language code.
+ *
+ * Returns: (nullable): a #GspellLanguage corresponding to @language_code, or
+ * %NULL if not found.
+ */
+const GspellLanguage *
+gspell_language_lookup (const gchar *language_code)
+{
+ const GspellLanguage *closest_match = NULL;
+ const GList *available_languages;
+ const GList *l;
+
+ g_return_val_if_fail (language_code != NULL, NULL);
+
+ available_languages = gspell_language_get_available ();
+
+ for (l = available_languages; l != NULL; l = l->next)
+ {
+ const GspellLanguage *language = l->data;
+ const gchar *code = language->code;
+ gsize length = strlen (code);
+
+ if (g_ascii_strcasecmp (language_code, code) == 0)
+ {
+ return language;
+ }
+
+ if (g_ascii_strncasecmp (language_code, code, length) == 0)
+ {
+ closest_match = language;
+ }
+ }
+
+ return closest_match;
+}
+
+/**
+ * gspell_language_get_code:
+ * @language: a #GspellLanguage.
+ *
+ * Returns: the @language code, for example fr_BE.
+ */
+const gchar *
+gspell_language_get_code (const GspellLanguage *language)
+{
+ g_return_val_if_fail (language != NULL, NULL);
+
+ return language->code;
+}
+
+/**
+ * gspell_language_get_name:
+ * @language: a #GspellLanguage.
+ *
+ * Returns the @language name translated to the current locale. For example
+ * "French (Belgium)" is returned if the current locale is in English and the
+ * @language code is fr_BE.
+ *
+ * Returns: the @language name.
+ */
+const gchar *
+gspell_language_get_name (const GspellLanguage *language)
+{
+ g_return_val_if_fail (language != NULL, NULL);
+
+ return language->name;
+}
+
+/**
+ * gspell_language_compare:
+ * @language_a: a #GspellLanguage.
+ * @language_b: another #GspellLanguage.
+ *
+ * Compares alphabetically two languages by their name, as returned by
+ * gspell_language_get_name().
+ *
+ * Returns: an integer less than, equal to, or greater than zero, if @language_a
+ * is <, == or > than @language_b.
+ */
+gint
+gspell_language_compare (const GspellLanguage *language_a,
+ const GspellLanguage *language_b)
+{
+ g_return_val_if_fail (language_a != NULL, 0);
+ g_return_val_if_fail (language_b != NULL, 0);
+
+ return g_strcmp0 (language_a->collate_key, language_b->collate_key);
+}
+
+/**
+ * gspell_language_copy:
+ * @language: a #GspellLanguage.
+ *
+ * Used by language bindings.
+ *
+ * Returns: a copy of @lang.
+ */
+GspellLanguage *
+gspell_language_copy (const GspellLanguage *language)
+{
+ g_return_val_if_fail (language != NULL, NULL);
+
+ return (GspellLanguage *) language;
+}
+
+/**
+ * gspell_language_free:
+ * @language: a #GspellLanguage.
+ *
+ * Used by language bindings.
+ */
+void
+gspell_language_free (GspellLanguage *language)
+{
+ g_return_if_fail (language != NULL);
+}
+
+/* ex:set ts=8 noet: */
diff --git a/gspell/gspell-language.h b/gspell/gspell-language.h
new file mode 100644
index 0000000..1d83d19
--- /dev/null
+++ b/gspell/gspell-language.h
@@ -0,0 +1,77 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2006 - Paolo Maggi
+ * Copyright 2008 - Novell, Inc.
+ * Copyright 2015, 2016 - Sébastien Wilmet
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+/* Based on GtkhtmlSpellLanguage (Novell), which was based on Marco Barisione's
+ * GSpellLanguage, which was based on GeditSpellCheckerLanguage, which was based
+ * partly on Epiphany's code.
+ */
+
+#ifndef GSPELL_LANGUAGE_H
+#define GSPELL_LANGUAGE_H
+
+#if !defined (GSPELL_H_INSIDE) && !defined (GSPELL_COMPILATION)
+#error "Only <gspell/gspell.h> can be included directly."
+#endif
+
+#include <glib-object.h>
+#include <gspell/gspell-version.h>
+
+G_BEGIN_DECLS
+
+typedef struct _GspellLanguage GspellLanguage;
+
+#define GSPELL_TYPE_LANGUAGE (gspell_language_get_type ())
+
+GSPELL_AVAILABLE_IN_ALL
+GType gspell_language_get_type (void) G_GNUC_CONST;
+
+GSPELL_AVAILABLE_IN_ALL
+const GList * gspell_language_get_available (void);
+
+GSPELL_AVAILABLE_IN_ALL
+const GspellLanguage *
+ gspell_language_get_default (void);
+
+GSPELL_AVAILABLE_IN_ALL
+const GspellLanguage *
+ gspell_language_lookup (const gchar *language_code);
+
+GSPELL_AVAILABLE_IN_ALL
+const gchar * gspell_language_get_code (const GspellLanguage *language);
+
+GSPELL_AVAILABLE_IN_ALL
+const gchar * gspell_language_get_name (const GspellLanguage *language);
+
+GSPELL_AVAILABLE_IN_ALL
+gint gspell_language_compare (const GspellLanguage *language_a,
+ const GspellLanguage *language_b);
+
+GSPELL_AVAILABLE_IN_ALL
+GspellLanguage *gspell_language_copy (const GspellLanguage *language);
+
+GSPELL_AVAILABLE_IN_ALL
+void gspell_language_free (GspellLanguage *language);
+
+G_END_DECLS
+
+#endif /* GSPELL_LANGUAGE_H */
+
+/* ex:set ts=8 noet: */
diff --git a/gspell/gspell-navigator-text-view.c b/gspell/gspell-navigator-text-view.c
new file mode 100644
index 0000000..166232d
--- /dev/null
+++ b/gspell/gspell-navigator-text-view.c
@@ -0,0 +1,553 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2015, 2016 - Sébastien Wilmet <swilmet gnome org>
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "gspell-navigator-text-view.h"
+#include <glib/gi18n-lib.h>
+#include "gspell-text-buffer.h"
+#include "gspell-text-iter.h"
+#include "gspell-utils.h"
+
+/**
+ * SECTION:navigator-text-view
+ * @Short_description: A GspellNavigator implementation for GtkTextView
+ * @Title: GspellNavigatorTextView
+ * @See_also: #GspellNavigator, #GspellCheckerDialog
+ *
+ * #GspellNavigatorTextView is a simple implementation of the
+ * #GspellNavigator interface for the #GtkTextView widget.
+ *
+ * If a selection exists in the #GtkTextView, only the selected text is spell
+ * checked. Otherwise the whole buffer is checked.
+ *
+ * If only the selected text is spell checked, the implementation of
+ * gspell_navigator_change_all() changes only the occurrences that were
+ * present in the selection.
+ *
+ * The implementation of gspell_navigator_goto_next() selects the
+ * misspelled word and scrolls to it.
+ *
+ * You need to call gspell_text_buffer_set_spell_checker() to associate a
+ * #GspellChecker to the #GtkTextBuffer.
+ */
+
+typedef struct _GspellNavigatorTextViewPrivate GspellNavigatorTextViewPrivate;
+
+struct _GspellNavigatorTextViewPrivate
+{
+ GtkTextView *view;
+ GtkTextBuffer *buffer;
+
+ /* Delimit the region to spell check. */
+ GtkTextMark *start_boundary;
+ GtkTextMark *end_boundary;
+
+ /* Current misspelled word. */
+ GtkTextMark *word_start;
+ GtkTextMark *word_end;
+};
+
+enum
+{
+ PROP_0,
+ PROP_VIEW,
+};
+
+static void gspell_navigator_iface_init (gpointer g_iface, gpointer iface_data);
+
+G_DEFINE_TYPE_WITH_CODE (GspellNavigatorTextView,
+ gspell_navigator_text_view,
+ G_TYPE_INITIALLY_UNOWNED,
+ G_ADD_PRIVATE (GspellNavigatorTextView)
+ G_IMPLEMENT_INTERFACE (GSPELL_TYPE_NAVIGATOR,
+ gspell_navigator_iface_init))
+
+static void
+init_boundaries (GspellNavigatorTextView *navigator)
+{
+ GspellNavigatorTextViewPrivate *priv;
+ GtkTextIter start;
+ GtkTextIter end;
+
+ priv = gspell_navigator_text_view_get_instance_private (navigator);
+
+ g_return_if_fail (priv->start_boundary == NULL);
+ g_return_if_fail (priv->end_boundary == NULL);
+
+ if (!gtk_text_buffer_get_selection_bounds (priv->buffer, &start, &end))
+ {
+ /* No selection, take the whole buffer. */
+ gtk_text_buffer_get_bounds (priv->buffer, &start, &end);
+ }
+
+ if (_gspell_text_iter_inside_word (&start) &&
+ !_gspell_text_iter_starts_word (&start))
+ {
+ _gspell_text_iter_backward_word_start (&start);
+ }
+
+ if (_gspell_text_iter_inside_word (&end))
+ {
+ _gspell_text_iter_forward_word_end (&end);
+ }
+
+ priv->start_boundary = gtk_text_buffer_create_mark (priv->buffer, NULL, &start, TRUE);
+ priv->end_boundary = gtk_text_buffer_create_mark (priv->buffer, NULL, &end, FALSE);
+}
+
+static void
+set_view (GspellNavigatorTextView *navigator,
+ GtkTextView *view)
+{
+ GspellNavigatorTextViewPrivate *priv;
+
+ priv = gspell_navigator_text_view_get_instance_private (navigator);
+
+ g_return_if_fail (priv->view == NULL);
+ g_return_if_fail (priv->buffer == NULL);
+
+ priv->view = g_object_ref (view);
+ priv->buffer = g_object_ref (gtk_text_view_get_buffer (view));
+
+ init_boundaries (navigator);
+
+ g_object_notify (G_OBJECT (navigator), "view");
+}
+
+static void
+gspell_navigator_text_view_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GspellNavigatorTextView *navigator = GSPELL_NAVIGATOR_TEXT_VIEW (object);
+
+ switch (prop_id)
+ {
+ case PROP_VIEW:
+ g_value_set_object (value, gspell_navigator_text_view_get_view (navigator));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gspell_navigator_text_view_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GspellNavigatorTextView *navigator = GSPELL_NAVIGATOR_TEXT_VIEW (object);
+
+ switch (prop_id)
+ {
+ case PROP_VIEW:
+ set_view (navigator, g_value_get_object (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gspell_navigator_text_view_dispose (GObject *object)
+{
+ GspellNavigatorTextViewPrivate *priv;
+
+ priv = gspell_navigator_text_view_get_instance_private (GSPELL_NAVIGATOR_TEXT_VIEW (object));
+
+ g_clear_object (&priv->view);
+
+ if (priv->buffer != NULL)
+ {
+ if (priv->start_boundary != NULL)
+ {
+ gtk_text_buffer_delete_mark (priv->buffer, priv->start_boundary);
+ priv->start_boundary = NULL;
+ }
+
+ if (priv->end_boundary != NULL)
+ {
+ gtk_text_buffer_delete_mark (priv->buffer, priv->end_boundary);
+ priv->end_boundary = NULL;
+ }
+
+ if (priv->word_start != NULL)
+ {
+ gtk_text_buffer_delete_mark (priv->buffer, priv->word_start);
+ priv->word_start = NULL;
+ }
+
+ if (priv->word_end != NULL)
+ {
+ gtk_text_buffer_delete_mark (priv->buffer, priv->word_end);
+ priv->word_end = NULL;
+ }
+
+ g_object_unref (priv->buffer);
+ priv->buffer = NULL;
+ }
+
+ G_OBJECT_CLASS (gspell_navigator_text_view_parent_class)->dispose (object);
+}
+
+static void
+gspell_navigator_text_view_class_init (GspellNavigatorTextViewClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->get_property = gspell_navigator_text_view_get_property;
+ object_class->set_property = gspell_navigator_text_view_set_property;
+ object_class->dispose = gspell_navigator_text_view_dispose;
+
+ /**
+ * GspellNavigatorTextView:view:
+ *
+ * The #GtkTextView. The buffer is not sufficient, the view is needed to
+ * scroll to the misspelled words.
+ */
+ g_object_class_install_property (object_class,
+ PROP_VIEW,
+ g_param_spec_object ("view",
+ "View",
+ "",
+ GTK_TYPE_TEXT_VIEW,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS));
+}
+
+static void
+gspell_navigator_text_view_init (GspellNavigatorTextView *self)
+{
+}
+
+static void
+select_misspelled_word (GspellNavigatorTextView *navigator)
+{
+ GspellNavigatorTextViewPrivate *priv;
+ GtkTextIter word_start;
+ GtkTextIter word_end;
+
+ priv = gspell_navigator_text_view_get_instance_private (navigator);
+
+ gtk_text_buffer_get_iter_at_mark (priv->buffer, &word_start, priv->word_start);
+ gtk_text_buffer_get_iter_at_mark (priv->buffer, &word_end, priv->word_end);
+
+ gtk_text_buffer_select_range (priv->buffer, &word_start, &word_end);
+
+ g_return_if_fail (gtk_text_view_get_buffer (priv->view) == priv->buffer);
+
+ gtk_text_view_scroll_to_mark (priv->view,
+ gtk_text_buffer_get_insert (priv->buffer),
+ 0.25,
+ FALSE,
+ 0.0,
+ 0.0);
+}
+
+static gboolean
+gspell_navigator_text_view_goto_next (GspellNavigator *navigator,
+ gchar **word_p,
+ GspellChecker **spell_checker_p,
+ GError **error_p)
+{
+ GspellNavigatorTextViewPrivate *priv;
+ GspellTextBuffer *gspell_buffer;
+ GspellChecker *spell_checker;
+ GtkTextIter word_start;
+ GtkTextIter end;
+ GtkTextTag *no_spell_check_tag;
+
+ priv = gspell_navigator_text_view_get_instance_private (GSPELL_NAVIGATOR_TEXT_VIEW (navigator));
+
+ g_assert ((priv->word_start == NULL && priv->word_end == NULL) ||
+ (priv->word_start != NULL && priv->word_end != NULL));
+
+ gspell_buffer = gspell_text_buffer_get_from_gtk_text_buffer (priv->buffer);
+ spell_checker = gspell_text_buffer_get_spell_checker (gspell_buffer);
+
+ if (spell_checker == NULL)
+ {
+ return FALSE;
+ }
+
+ if (gspell_checker_get_language (spell_checker) == NULL)
+ {
+ if (spell_checker_p != NULL)
+ {
+ *spell_checker_p = g_object_ref (spell_checker);
+ }
+
+ g_set_error (error_p,
+ GSPELL_CHECKER_ERROR,
+ GSPELL_CHECKER_ERROR_NO_LANGUAGE_SET,
+ "%s",
+ _("Spell checker error: no language set. "
+ "It’s maybe because no dictionaries are installed."));
+
+ return FALSE;
+ }
+
+ gtk_text_buffer_get_iter_at_mark (priv->buffer, &end, priv->end_boundary);
+
+ if (priv->word_start == NULL)
+ {
+ GtkTextIter start;
+
+ gtk_text_buffer_get_iter_at_mark (priv->buffer, &start, priv->start_boundary);
+
+ priv->word_start = gtk_text_buffer_create_mark (priv->buffer, NULL, &start, TRUE);
+ priv->word_end = gtk_text_buffer_create_mark (priv->buffer, NULL, &start, FALSE);
+
+ word_start = start;
+ }
+ else
+ {
+ GtkTextIter word_end;
+
+ gtk_text_buffer_get_iter_at_mark (priv->buffer, &word_end, priv->word_end);
+
+ if (gtk_text_iter_compare (&end, &word_end) <= 0)
+ {
+ return FALSE;
+ }
+
+ word_start = word_end;
+ }
+
+ no_spell_check_tag = _gspell_utils_get_no_spell_check_tag (priv->buffer);
+
+ while (TRUE)
+ {
+ GtkTextIter word_end;
+ gchar *word;
+ gboolean correctly_spelled;
+ GError *error = NULL;
+
+ if (!_gspell_text_iter_starts_word (&word_start))
+ {
+ GtkTextIter iter;
+
+ iter = word_start;
+ _gspell_text_iter_forward_word_end (&word_start);
+
+ if (gtk_text_iter_equal (&iter, &word_start))
+ {
+ /* Didn't move, we are at the end. */
+ return FALSE;
+ }
+
+ _gspell_text_iter_backward_word_start (&word_start);
+ }
+
+ if (!_gspell_utils_skip_no_spell_check (no_spell_check_tag, &word_start, &end))
+ {
+ return FALSE;
+ }
+
+ g_return_val_if_fail (_gspell_text_iter_starts_word (&word_start), FALSE);
+
+ word_end = word_start;
+ _gspell_text_iter_forward_word_end (&word_end);
+
+ if (gtk_text_iter_compare (&end, &word_end) < 0)
+ {
+ return FALSE;
+ }
+
+ word = gtk_text_buffer_get_text (priv->buffer, &word_start, &word_end, FALSE);
+
+ correctly_spelled = gspell_checker_check_word (spell_checker, word, -1, &error);
+
+ if (error != NULL)
+ {
+ g_propagate_error (error_p, error);
+ g_free (word);
+ return FALSE;
+ }
+
+ if (!correctly_spelled)
+ {
+ /* Found! */
+ gtk_text_buffer_move_mark (priv->buffer, priv->word_start, &word_start);
+ gtk_text_buffer_move_mark (priv->buffer, priv->word_end, &word_end);
+
+ select_misspelled_word (GSPELL_NAVIGATOR_TEXT_VIEW (navigator));
+
+ if (spell_checker_p != NULL)
+ {
+ *spell_checker_p = g_object_ref (spell_checker);
+ }
+
+ if (word_p != NULL)
+ {
+ *word_p = word;
+ }
+ else
+ {
+ g_free (word);
+ }
+
+ return TRUE;
+ }
+
+ word_start = word_end;
+ g_free (word);
+ }
+
+ return FALSE;
+}
+
+static void
+gspell_navigator_text_view_change (GspellNavigator *navigator,
+ const gchar *word,
+ const gchar *change_to)
+{
+ GspellNavigatorTextViewPrivate *priv;
+ GtkTextIter word_start;
+ GtkTextIter word_end;
+ gchar *word_in_buffer = NULL;
+
+ priv = gspell_navigator_text_view_get_instance_private (GSPELL_NAVIGATOR_TEXT_VIEW (navigator));
+
+ g_return_if_fail (GTK_IS_TEXT_MARK (priv->word_start));
+ g_return_if_fail (GTK_IS_TEXT_MARK (priv->word_end));
+
+ gtk_text_buffer_get_iter_at_mark (priv->buffer, &word_start, priv->word_start);
+ gtk_text_buffer_get_iter_at_mark (priv->buffer, &word_end, priv->word_end);
+
+ word_in_buffer = gtk_text_buffer_get_slice (priv->buffer, &word_start, &word_end, TRUE);
+ g_return_if_fail (word_in_buffer != NULL);
+ g_return_if_fail (g_strcmp0 (word_in_buffer, word) == 0);
+ g_free (word_in_buffer);
+
+ gtk_text_buffer_begin_user_action (priv->buffer);
+
+ gtk_text_buffer_delete (priv->buffer, &word_start, &word_end);
+ gtk_text_buffer_insert (priv->buffer, &word_start, change_to, -1);
+
+ gtk_text_buffer_end_user_action (priv->buffer);
+}
+
+static void
+gspell_navigator_text_view_change_all (GspellNavigator *navigator,
+ const gchar *word,
+ const gchar *change_to)
+{
+ GspellNavigatorTextViewPrivate *priv;
+ GtkTextIter iter;
+
+ priv = gspell_navigator_text_view_get_instance_private (GSPELL_NAVIGATOR_TEXT_VIEW (navigator));
+
+ g_return_if_fail (GTK_IS_TEXT_MARK (priv->start_boundary));
+ g_return_if_fail (GTK_IS_TEXT_MARK (priv->end_boundary));
+
+ gtk_text_buffer_get_iter_at_mark (priv->buffer, &iter, priv->start_boundary);
+
+ gtk_text_buffer_begin_user_action (priv->buffer);
+
+ while (TRUE)
+ {
+ gboolean found;
+ GtkTextIter match_start;
+ GtkTextIter match_end;
+ GtkTextIter limit;
+
+ gtk_text_buffer_get_iter_at_mark (priv->buffer, &limit, priv->end_boundary);
+
+ found = gtk_text_iter_forward_search (&iter,
+ word,
+ GTK_TEXT_SEARCH_VISIBLE_ONLY |
+ GTK_TEXT_SEARCH_TEXT_ONLY,
+ &match_start,
+ &match_end,
+ &limit);
+
+ if (!found)
+ {
+ break;
+ }
+
+ if (_gspell_text_iter_starts_word (&match_start) &&
+ _gspell_text_iter_ends_word (&match_end))
+ {
+ gtk_text_buffer_delete (priv->buffer, &match_start, &match_end);
+ gtk_text_buffer_insert (priv->buffer, &match_end, change_to, -1);
+ }
+
+ iter = match_end;
+ }
+
+ gtk_text_buffer_end_user_action (priv->buffer);
+}
+
+static void
+gspell_navigator_iface_init (gpointer g_iface,
+ gpointer iface_data)
+{
+ GspellNavigatorInterface *iface = g_iface;
+
+ iface->goto_next = gspell_navigator_text_view_goto_next;
+ iface->change = gspell_navigator_text_view_change;
+ iface->change_all = gspell_navigator_text_view_change_all;
+}
+
+/**
+ * gspell_navigator_text_view_new:
+ * @view: a #GtkTextView.
+ *
+ * Returns: (transfer floating): a new #GspellNavigatorTextView floating object.
+ */
+GspellNavigator *
+gspell_navigator_text_view_new (GtkTextView *view)
+{
+ g_return_val_if_fail (GTK_IS_TEXT_VIEW (view), NULL);
+
+ return g_object_new (GSPELL_TYPE_NAVIGATOR_TEXT_VIEW,
+ "view", view,
+ NULL);
+}
+
+/**
+ * gspell_navigator_text_view_get_view:
+ * @navigator: a #GspellNavigatorTextView.
+ *
+ * Returns: (transfer none): the #GtkTextView.
+ */
+GtkTextView *
+gspell_navigator_text_view_get_view (GspellNavigatorTextView *navigator)
+{
+ GspellNavigatorTextViewPrivate *priv;
+
+ g_return_val_if_fail (GSPELL_IS_NAVIGATOR_TEXT_VIEW (navigator), NULL);
+
+ priv = gspell_navigator_text_view_get_instance_private (navigator);
+ return priv->view;
+}
+
+/* ex:set ts=8 noet: */
diff --git a/gspell/gspell-navigator-text-view.h b/gspell/gspell-navigator-text-view.h
new file mode 100644
index 0000000..53cfd78
--- /dev/null
+++ b/gspell/gspell-navigator-text-view.h
@@ -0,0 +1,59 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2015, 2016 - Sébastien Wilmet <swilmet gnome org>
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GSPELL_NAVIGATOR_TEXT_VIEW_H
+#define GSPELL_NAVIGATOR_TEXT_VIEW_H
+
+#if !defined (GSPELL_H_INSIDE) && !defined (GSPELL_COMPILATION)
+#error "Only <gspell/gspell.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+#include <gspell/gspell-navigator.h>
+#include <gspell/gspell-checker.h>
+#include <gspell/gspell-version.h>
+
+G_BEGIN_DECLS
+
+#define GSPELL_TYPE_NAVIGATOR_TEXT_VIEW (gspell_navigator_text_view_get_type ())
+
+GSPELL_AVAILABLE_IN_ALL
+G_DECLARE_DERIVABLE_TYPE (GspellNavigatorTextView, gspell_navigator_text_view,
+ GSPELL, NAVIGATOR_TEXT_VIEW,
+ GInitiallyUnowned)
+
+struct _GspellNavigatorTextViewClass
+{
+ GInitiallyUnownedClass parent_class;
+
+ /* Padding for future expansion */
+ gpointer padding[8];
+};
+
+GSPELL_AVAILABLE_IN_ALL
+GspellNavigator * gspell_navigator_text_view_new (GtkTextView *view);
+
+GSPELL_AVAILABLE_IN_ALL
+GtkTextView * gspell_navigator_text_view_get_view (GspellNavigatorTextView *navigator);
+
+G_END_DECLS
+
+#endif /* GSPELL_NAVIGATOR_TEXT_VIEW_H */
+
+/* ex:set ts=8 noet: */
diff --git a/gspell/gspell-navigator.c b/gspell/gspell-navigator.c
new file mode 100644
index 0000000..9029053
--- /dev/null
+++ b/gspell/gspell-navigator.c
@@ -0,0 +1,171 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2015, 2016, 2017 - Sébastien Wilmet <swilmet gnome org>
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "gspell-navigator.h"
+
+/**
+ * SECTION:navigator
+ * @Short_description: Interface to navigate through misspelled words
+ * @Title: GspellNavigator
+ * @See_also: #GspellNavigatorTextView, #GspellCheckerDialog
+ *
+ * #GspellNavigator is an interface to navigate through misspelled words,
+ * and correct the mistakes.
+ *
+ * It is used by widgets like #GspellCheckerDialog. The purpose is to
+ * spell-check a document one word at a time.
+ *
+ * It is not mandatory to navigate through all the text. Depending on the
+ * context, an implementation could spell-check only the current page, or the
+ * selection, etc.
+ *
+ * For #GtkTextView, see the #GspellNavigatorTextView implementation of this
+ * interface.
+ *
+ * The #GspellNavigator interface requires #GInitiallyUnowned because a
+ * #GspellNavigator object is meant to be passed as an argument to a #GtkWidget
+ * constructor, for example gspell_checker_dialog_new(). This permits to
+ * decouple the frontend from the backend, making the #GtkWidget re-usable for
+ * different #GspellNavigator's.
+ */
+
+G_DEFINE_INTERFACE (GspellNavigator, gspell_navigator, G_TYPE_INITIALLY_UNOWNED)
+
+static gboolean
+gspell_navigator_goto_next_default (GspellNavigator *navigator,
+ gchar **word,
+ GspellChecker **spell_checker,
+ GError **error)
+{
+ return FALSE;
+}
+
+static void
+gspell_navigator_change_default (GspellNavigator *navigator,
+ const gchar *word,
+ const gchar *change_to)
+{
+}
+
+static void
+gspell_navigator_change_all_default (GspellNavigator *navigator,
+ const gchar *word,
+ const gchar *change_to)
+{
+}
+
+static void
+gspell_navigator_default_init (GspellNavigatorInterface *iface)
+{
+ iface->goto_next = gspell_navigator_goto_next_default;
+ iface->change = gspell_navigator_change_default;
+ iface->change_all = gspell_navigator_change_all_default;
+}
+
+/**
+ * gspell_navigator_goto_next:
+ * @navigator: a #GspellNavigator.
+ * @word: (out) (optional): a location to store an allocated string, or %NULL.
+ * Use g_free() to free the returned string.
+ * @spell_checker: (out) (optional) (transfer full): a location to store the
+ * #GspellChecker used, or %NULL. Use g_object_unref() when no longer
+ * needed.
+ * @error: (out) (optional): a location to a %NULL #GError, or %NULL.
+ *
+ * Goes to the next misspelled word. When called the first time, goes to the
+ * first misspelled word.
+ *
+ * Returns: %TRUE if a next misspelled word has been found, %FALSE if the spell
+ * checking is finished or if an error occurred.
+ */
+gboolean
+gspell_navigator_goto_next (GspellNavigator *navigator,
+ gchar **word,
+ GspellChecker **spell_checker,
+ GError **error)
+{
+ g_return_val_if_fail (GSPELL_IS_NAVIGATOR (navigator), FALSE);
+ g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+
+ if (word != NULL)
+ {
+ *word = NULL;
+ }
+
+ if (spell_checker != NULL)
+ {
+ *spell_checker = NULL;
+ }
+
+ return GSPELL_NAVIGATOR_GET_IFACE (navigator)->goto_next (navigator,
+ word,
+ spell_checker,
+ error);
+}
+
+/**
+ * gspell_navigator_change:
+ * @navigator: a #GspellNavigator.
+ * @word: the word to change.
+ * @change_to: the replacement.
+ *
+ * Changes the current @word by @change_to in the text. @word must be the same
+ * as returned by the last call to gspell_navigator_goto_next().
+ *
+ * This function doesn't call gspell_checker_set_correction(). A widget using a
+ * #GspellNavigator should call gspell_checker_set_correction() in addition to
+ * this function.
+ */
+void
+gspell_navigator_change (GspellNavigator *navigator,
+ const gchar *word,
+ const gchar *change_to)
+{
+ g_return_if_fail (GSPELL_IS_NAVIGATOR (navigator));
+
+ GSPELL_NAVIGATOR_GET_IFACE (navigator)->change (navigator, word, change_to);
+}
+
+/**
+ * gspell_navigator_change_all:
+ * @navigator: a #GspellNavigator.
+ * @word: the word to change.
+ * @change_to: the replacement.
+ *
+ * Changes all occurrences of @word by @change_to in the text.
+ *
+ * This function doesn't call gspell_checker_set_correction(). A widget using a
+ * #GspellNavigator should call gspell_checker_set_correction() in addition to
+ * this function.
+ */
+void
+gspell_navigator_change_all (GspellNavigator *navigator,
+ const gchar *word,
+ const gchar *change_to)
+{
+ g_return_if_fail (GSPELL_IS_NAVIGATOR (navigator));
+
+ GSPELL_NAVIGATOR_GET_IFACE (navigator)->change_all (navigator, word, change_to);
+}
+
+/* ex:set ts=8 noet: */
diff --git a/gspell/gspell-navigator.h b/gspell/gspell-navigator.h
new file mode 100644
index 0000000..211abf6
--- /dev/null
+++ b/gspell/gspell-navigator.h
@@ -0,0 +1,78 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2015, 2016 - Sébastien Wilmet <swilmet gnome org>
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GSPELL_NAVIGATOR_H
+#define GSPELL_NAVIGATOR_H
+
+#if !defined (GSPELL_H_INSIDE) && !defined (GSPELL_COMPILATION)
+#error "Only <gspell/gspell.h> can be included directly."
+#endif
+
+#include <glib-object.h>
+#include <gspell/gspell-checker.h>
+#include <gspell/gspell-version.h>
+
+G_BEGIN_DECLS
+
+#define GSPELL_TYPE_NAVIGATOR (gspell_navigator_get_type ())
+
+GSPELL_AVAILABLE_IN_ALL
+G_DECLARE_INTERFACE (GspellNavigator, gspell_navigator,
+ GSPELL, NAVIGATOR,
+ GInitiallyUnowned)
+
+struct _GspellNavigatorInterface
+{
+ GTypeInterface parent_interface;
+
+ gboolean (* goto_next) (GspellNavigator *navigator,
+ gchar **word,
+ GspellChecker **spell_checker,
+ GError **error);
+
+ void (* change) (GspellNavigator *navigator,
+ const gchar *word,
+ const gchar *change_to);
+
+ void (* change_all) (GspellNavigator *navigator,
+ const gchar *word,
+ const gchar *change_to);
+};
+
+GSPELL_AVAILABLE_IN_ALL
+gboolean gspell_navigator_goto_next (GspellNavigator *navigator,
+ gchar **word,
+ GspellChecker **spell_checker,
+ GError **error);
+
+GSPELL_AVAILABLE_IN_ALL
+void gspell_navigator_change (GspellNavigator *navigator,
+ const gchar *word,
+ const gchar *change_to);
+
+GSPELL_AVAILABLE_IN_ALL
+void gspell_navigator_change_all (GspellNavigator *navigator,
+ const gchar *word,
+ const gchar *change_to);
+
+G_END_DECLS
+
+#endif /* GSPELL_NAVIGATOR_H */
+
+/* ex:set ts=8 noet: */
diff --git a/gspell/gspell-osx.c b/gspell/gspell-osx.c
new file mode 100644
index 0000000..e3b9615
--- /dev/null
+++ b/gspell/gspell-osx.c
@@ -0,0 +1,68 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2011, 2014 - Jesse van den Kieboom
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "gspell-osx.h"
+#include <gtkosxapplication.h>
+#import <Cocoa/Cocoa.h>
+
+gchar *
+_gspell_osx_get_resource_path (void)
+{
+ gchar *id;
+ gchar *ret = NULL;
+
+ id = gtkosx_application_get_bundle_id ();
+
+ if (id != NULL)
+ {
+ ret = gtkosx_application_get_resource_path ();
+ }
+
+ g_free (id);
+ return ret;
+}
+
+gchar *
+_gspell_osx_get_preferred_spell_language ()
+{
+ gchar *ret = NULL;
+ NSAutoreleasePool *pool;
+
+ pool = [[NSAutoreleasePool alloc] init];
+
+#if defined(MAC_OS_X_VERSION_10_5) && MAC_OS_X_VERSION_MIN_REQUIRED > MAC_OS_X_VERSION_10_5
+ NSArray *langs;
+
+ langs = [[NSSpellChecker sharedSpellChecker] userPreferredLanguages];
+
+ if ([langs count] > 0)
+ {
+ ret = g_strdup ([[langs objectAtIndex:0] UTF8String]);
+ }
+#endif
+
+ [pool release];
+ return ret;
+}
+
+/* ex:set ts=8 noet: */
diff --git a/gspell/gspell-osx.h b/gspell/gspell-osx.h
new file mode 100644
index 0000000..a53aea3
--- /dev/null
+++ b/gspell/gspell-osx.h
@@ -0,0 +1,38 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2011, 2014 - Jesse van den Kieboom
+ * Copyright 2015 - Sébastien Wilmet
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef _GSPELL_OSX_H
+#define _GSPELL_OSX_H
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+G_GNUC_INTERNAL
+gchar * _gspell_osx_get_preferred_spell_language (void);
+
+G_GNUC_INTERNAL
+gchar * _gspell_osx_get_resource_path (void);
+
+G_END_DECLS
+
+#endif /* _GSPELL_OSX_H */
+
+/* ex:set ts=8 noet: */
diff --git a/gspell/gspell-text-buffer.c b/gspell/gspell-text-buffer.c
new file mode 100644
index 0000000..74bb6a9
--- /dev/null
+++ b/gspell/gspell-text-buffer.c
@@ -0,0 +1,275 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2015, 2016, 2017 - Sébastien Wilmet
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "gspell-text-buffer.h"
+
+/**
+ * SECTION:text-buffer
+ * @Title: GspellTextBuffer
+ * @Short_description: Spell checking support for GtkTextBuffer
+ *
+ * #GspellTextBuffer extends the #GtkTextBuffer class but without subclassing
+ * it, because the GtkSourceView library has already a #GtkTextBuffer subclass.
+ *
+ * # Support of the no-spell-check tag defined by GtkSourceView
+ *
+ * The syntax highlighting engine of the [GtkSourceView
+ * library](https://wiki.gnome.org/Projects/GtkSourceView) has a feature called
+ * “context classes”. One of the standard context classes is
+ * “<emphasis>no-spell-check</emphasis>”: it defines the regions in the
+ * #GtkTextBuffer that should not be spell-checked.
+ *
+ * GtkSourceView creates a #GtkTextTag named
+ * `"gtksourceview:context-classes:no-spell-check"`. gspell reads this tag, to
+ * skip the text contained within the tag.
+ *
+ * If you use the GtkSourceView library in your application, keep in mind that
+ * the #GtkTextTag created by GtkSourceView is for read-only purposes; you
+ * cannot apply it yourself to other regions.
+ *
+ * On the other hand if the GtkSourceView library is not used, you can create a
+ * #GtkTextTag with the same name to mark certain regions in the text that
+ * gspell should skip. As it is not a great API, it is
+ * [planned](https://bugzilla.gnome.org/show_bug.cgi?id=771582) to add an
+ * explicit API in #GspellTextBuffer to set a #GtkTextTag that gspell should
+ * skip.
+ *
+ * See the class description of #GtkSourceBuffer for more information about
+ * context classes.
+ */
+
+struct _GspellTextBuffer
+{
+ GObject parent;
+
+ GtkTextBuffer *buffer;
+ GspellChecker *spell_checker;
+};
+
+enum
+{
+ PROP_0,
+ PROP_BUFFER,
+ PROP_SPELL_CHECKER,
+};
+
+#define GSPELL_TEXT_BUFFER_KEY "gspell-text-buffer-key"
+
+G_DEFINE_TYPE (GspellTextBuffer, gspell_text_buffer, G_TYPE_OBJECT)
+
+static void
+gspell_text_buffer_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GspellTextBuffer *gspell_buffer = GSPELL_TEXT_BUFFER (object);
+
+ switch (prop_id)
+ {
+ case PROP_BUFFER:
+ g_value_set_object (value, gspell_text_buffer_get_buffer (gspell_buffer));
+ break;
+
+ case PROP_SPELL_CHECKER:
+ g_value_set_object (value, gspell_text_buffer_get_spell_checker (gspell_buffer));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gspell_text_buffer_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GspellTextBuffer *gspell_buffer = GSPELL_TEXT_BUFFER (object);
+
+ switch (prop_id)
+ {
+ case PROP_BUFFER:
+ g_assert (gspell_buffer->buffer == NULL);
+ gspell_buffer->buffer = g_value_get_object (value);
+ break;
+
+ case PROP_SPELL_CHECKER:
+ gspell_text_buffer_set_spell_checker (gspell_buffer, g_value_get_object (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gspell_text_buffer_dispose (GObject *object)
+{
+ GspellTextBuffer *gspell_buffer = GSPELL_TEXT_BUFFER (object);
+
+ gspell_buffer->buffer = NULL;
+ g_clear_object (&gspell_buffer->spell_checker);
+
+ G_OBJECT_CLASS (gspell_text_buffer_parent_class)->dispose (object);
+}
+
+static void
+gspell_text_buffer_class_init (GspellTextBufferClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->get_property = gspell_text_buffer_get_property;
+ object_class->set_property = gspell_text_buffer_set_property;
+ object_class->dispose = gspell_text_buffer_dispose;
+
+ /**
+ * GspellTextBuffer:buffer:
+ *
+ * The #GtkTextBuffer.
+ */
+ g_object_class_install_property (object_class,
+ PROP_BUFFER,
+ g_param_spec_object ("buffer",
+ "Buffer",
+ "",
+ GTK_TYPE_TEXT_BUFFER,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * GspellTextBuffer:spell-checker:
+ *
+ * The #GspellChecker.
+ */
+ g_object_class_install_property (object_class,
+ PROP_SPELL_CHECKER,
+ g_param_spec_object ("spell-checker",
+ "Spell Checker",
+ "",
+ GSPELL_TYPE_CHECKER,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+}
+
+static void
+gspell_text_buffer_init (GspellTextBuffer *gspell_buffer)
+{
+}
+
+/**
+ * gspell_text_buffer_get_from_gtk_text_buffer:
+ * @gtk_buffer: a #GtkTextBuffer.
+ *
+ * Returns the #GspellTextBuffer of @gtk_buffer. The returned object is
+ * guaranteed to be the same for the lifetime of @gtk_buffer.
+ *
+ * Returns: (transfer none): the #GspellTextBuffer of @gtk_buffer.
+ */
+/* Yes I know, the function name is a bit long. But at least there is no
+ * possible confusions. Other names that came to my mind:
+ * - get_from_buffer(), but it's confusing: which buffer is it?
+ * - get_from_sibling(): less clear.
+ */
+GspellTextBuffer *
+gspell_text_buffer_get_from_gtk_text_buffer (GtkTextBuffer *gtk_buffer)
+{
+ GspellTextBuffer *gspell_buffer;
+
+ g_return_val_if_fail (GTK_IS_TEXT_BUFFER (gtk_buffer), NULL);
+
+ gspell_buffer = g_object_get_data (G_OBJECT (gtk_buffer), GSPELL_TEXT_BUFFER_KEY);
+
+ if (gspell_buffer == NULL)
+ {
+ gspell_buffer = g_object_new (GSPELL_TYPE_TEXT_BUFFER,
+ "buffer", gtk_buffer,
+ NULL);
+
+ g_object_set_data_full (G_OBJECT (gtk_buffer),
+ GSPELL_TEXT_BUFFER_KEY,
+ gspell_buffer,
+ g_object_unref);
+ }
+
+ g_return_val_if_fail (GSPELL_IS_TEXT_BUFFER (gspell_buffer), NULL);
+ return gspell_buffer;
+}
+
+/**
+ * gspell_text_buffer_get_buffer:
+ * @gspell_buffer: a #GspellTextBuffer.
+ *
+ * Returns: (transfer none): the #GtkTextBuffer of @gspell_buffer.
+ */
+GtkTextBuffer *
+gspell_text_buffer_get_buffer (GspellTextBuffer *gspell_buffer)
+{
+ g_return_val_if_fail (GSPELL_IS_TEXT_BUFFER (gspell_buffer), NULL);
+
+ return gspell_buffer->buffer;
+}
+
+/**
+ * gspell_text_buffer_get_spell_checker:
+ * @gspell_buffer: a #GspellTextBuffer.
+ *
+ * Returns: (nullable) (transfer none): the #GspellChecker if one has been set,
+ * or %NULL.
+ */
+GspellChecker *
+gspell_text_buffer_get_spell_checker (GspellTextBuffer *gspell_buffer)
+{
+ g_return_val_if_fail (GSPELL_IS_TEXT_BUFFER (gspell_buffer), NULL);
+
+ return gspell_buffer->spell_checker;
+}
+
+/**
+ * gspell_text_buffer_set_spell_checker:
+ * @gspell_buffer: a #GspellTextBuffer.
+ * @spell_checker: (nullable): a #GspellChecker, or %NULL to unset the spell
+ * checker.
+ *
+ * Sets a #GspellChecker to a #GspellTextBuffer. The @gspell_buffer will own a
+ * reference to @spell_checker, so you can release your reference to
+ * @spell_checker if you no longer need it.
+ */
+void
+gspell_text_buffer_set_spell_checker (GspellTextBuffer *gspell_buffer,
+ GspellChecker *spell_checker)
+{
+ g_return_if_fail (GSPELL_IS_TEXT_BUFFER (gspell_buffer));
+ g_return_if_fail (spell_checker == NULL || GSPELL_IS_CHECKER (spell_checker));
+
+ if (g_set_object (&gspell_buffer->spell_checker, spell_checker))
+ {
+ g_object_notify (G_OBJECT (gspell_buffer), "spell-checker");
+ }
+}
+
+/* ex:set ts=8 noet: */
diff --git a/gspell/gspell-text-buffer.h b/gspell/gspell-text-buffer.h
new file mode 100644
index 0000000..2b5e7ac
--- /dev/null
+++ b/gspell/gspell-text-buffer.h
@@ -0,0 +1,57 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2015, 2016 - Sébastien Wilmet
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GSPELL_TEXT_BUFFER_H
+#define GSPELL_TEXT_BUFFER_H
+
+#if !defined (GSPELL_H_INSIDE) && !defined (GSPELL_COMPILATION)
+#error "Only <gspell/gspell.h> can be included directly."
+#endif
+
+#include <gspell/gspell-checker.h>
+#include <gspell/gspell-version.h>
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GSPELL_TYPE_TEXT_BUFFER (gspell_text_buffer_get_type ())
+
+GSPELL_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (GspellTextBuffer, gspell_text_buffer,
+ GSPELL, TEXT_BUFFER,
+ GObject)
+
+GSPELL_AVAILABLE_IN_ALL
+GspellTextBuffer * gspell_text_buffer_get_from_gtk_text_buffer (GtkTextBuffer *gtk_buffer);
+
+GSPELL_AVAILABLE_IN_ALL
+GtkTextBuffer * gspell_text_buffer_get_buffer (GspellTextBuffer
*gspell_buffer);
+
+GSPELL_AVAILABLE_IN_ALL
+GspellChecker * gspell_text_buffer_get_spell_checker (GspellTextBuffer
*gspell_buffer);
+
+GSPELL_AVAILABLE_IN_ALL
+void gspell_text_buffer_set_spell_checker (GspellTextBuffer *gspell_buffer,
+ GspellChecker *spell_checker);
+
+G_END_DECLS
+
+#endif /* GSPELL_TEXT_BUFFER_H */
+
+/* ex:set ts=8 noet: */
diff --git a/gspell/gspell-text-iter.c b/gspell/gspell-text-iter.c
new file mode 100644
index 0000000..70173c8
--- /dev/null
+++ b/gspell/gspell-text-iter.c
@@ -0,0 +1,193 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2016 - Sébastien Wilmet <swilmet gnome org>
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "gspell-text-iter.h"
+#include "gspell-utils.h"
+
+/* The same functions as the gtk_text_iter_* equivalents, but take into account:
+ * - Word contractions with an apostrophe. For example "doesn't", which is a
+ * contraction of the two words "does not".
+ * - Compounds with words separated by dashes. For example "spell-checking".
+ *
+ * When to include an apostrophe or a dash in a word? The heuristic is that the
+ * apostrophe must be surrounded by a pango-defined word on *each* side of the
+ * apostrophe. In other words, there must be a word end on the left side and a
+ * word start on the right side.
+ *
+ * Note that with that rule, a word can contain several apostrophes or dashes,
+ * like "rock'n'roll". Usually such a word would be considered as misspelled,
+ * but it's important to take every apostrophes, otherwise the word boundaries
+ * would change depending on the GtkTextIter location, which would lead to bugs.
+ *
+ * Possible improvement: support words like "doin'" or "'til". That is, if the
+ * "internal" word ("doin" or "til") is surrounded by only one apostrophe, take
+ * the apostrophe. The implementation would be slightly more complicated, since
+ * a function behavior depends on the other side of the word.
+ *
+ * When doing changes to the algo here, it should be reflected for the GtkEntry
+ * support as well, to have a consistent behavior.
+ *
+ * TODO: the following Pango bug is now mostly done, see if the gtk_text_iter_*
+ * functions can be used directly, or if the code here can be simplified.
+ * https://bugzilla.gnome.org/show_bug.cgi?id=97545
+ * "Make pango_default_break follow Unicode TR #29"
+ */
+
+static gboolean
+is_apostrophe_or_dash (const GtkTextIter *iter)
+{
+ gunichar ch;
+
+ ch = gtk_text_iter_get_char (iter);
+
+ return _gspell_utils_is_apostrophe_or_dash (ch);
+}
+
+gboolean
+_gspell_text_iter_forward_word_end (GtkTextIter *iter)
+{
+ g_return_val_if_fail (iter != NULL, FALSE);
+
+ while (gtk_text_iter_forward_word_end (iter))
+ {
+ GtkTextIter next_char;
+
+ if (!is_apostrophe_or_dash (iter))
+ {
+ return TRUE;
+ }
+
+ next_char = *iter;
+ gtk_text_iter_forward_char (&next_char);
+
+ if (!gtk_text_iter_starts_word (&next_char))
+ {
+ return TRUE;
+ }
+
+ *iter = next_char;
+ }
+
+ return FALSE;
+}
+
+gboolean
+_gspell_text_iter_backward_word_start (GtkTextIter *iter)
+{
+ g_return_val_if_fail (iter != NULL, FALSE);
+
+ while (gtk_text_iter_backward_word_start (iter))
+ {
+ GtkTextIter prev_char = *iter;
+
+ if (!gtk_text_iter_backward_char (&prev_char) ||
+ !is_apostrophe_or_dash (&prev_char) ||
+ !gtk_text_iter_ends_word (&prev_char))
+ {
+ return TRUE;
+ }
+
+ *iter = prev_char;
+ }
+
+ return FALSE;
+}
+
+gboolean
+_gspell_text_iter_starts_word (const GtkTextIter *iter)
+{
+ GtkTextIter prev_char;
+
+ g_return_val_if_fail (iter != NULL, FALSE);
+
+ if (!gtk_text_iter_starts_word (iter))
+ {
+ return FALSE;
+ }
+
+ prev_char = *iter;
+ if (!gtk_text_iter_backward_char (&prev_char))
+ {
+ return TRUE;
+ }
+
+ if (is_apostrophe_or_dash (&prev_char) &&
+ gtk_text_iter_ends_word (&prev_char))
+ {
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+gboolean
+_gspell_text_iter_ends_word (const GtkTextIter *iter)
+{
+ GtkTextIter next_char;
+
+ g_return_val_if_fail (iter != NULL, FALSE);
+
+ if (!gtk_text_iter_ends_word (iter))
+ {
+ return FALSE;
+ }
+
+ if (gtk_text_iter_is_end (iter))
+ {
+ return TRUE;
+ }
+
+ next_char = *iter;
+ gtk_text_iter_forward_char (&next_char);
+
+ if (is_apostrophe_or_dash (iter) &&
+ gtk_text_iter_starts_word (&next_char))
+ {
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+gboolean
+_gspell_text_iter_inside_word (const GtkTextIter *iter)
+{
+ g_return_val_if_fail (iter != NULL, FALSE);
+
+ if (gtk_text_iter_inside_word (iter))
+ {
+ return TRUE;
+ }
+
+ if (gtk_text_iter_ends_word (iter) &&
+ is_apostrophe_or_dash (iter))
+ {
+ GtkTextIter next_char = *iter;
+ gtk_text_iter_forward_char (&next_char);
+ return gtk_text_iter_starts_word (&next_char);
+ }
+
+ return FALSE;
+}
+
+/* ex:set ts=8 noet: */
diff --git a/gspell/gspell-text-iter.h b/gspell/gspell-text-iter.h
new file mode 100644
index 0000000..5f332aa
--- /dev/null
+++ b/gspell/gspell-text-iter.h
@@ -0,0 +1,46 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2016 - Sébastien Wilmet <swilmet gnome org>
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GSPELL_TEXT_ITER_H
+#define GSPELL_TEXT_ITER_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+G_GNUC_INTERNAL
+gboolean _gspell_text_iter_forward_word_end (GtkTextIter *iter);
+
+G_GNUC_INTERNAL
+gboolean _gspell_text_iter_backward_word_start (GtkTextIter *iter);
+
+G_GNUC_INTERNAL
+gboolean _gspell_text_iter_starts_word (const GtkTextIter *iter);
+
+G_GNUC_INTERNAL
+gboolean _gspell_text_iter_ends_word (const GtkTextIter *iter);
+
+G_GNUC_INTERNAL
+gboolean _gspell_text_iter_inside_word (const GtkTextIter *iter);
+
+G_END_DECLS
+
+#endif /* GSPELL_TEXT_ITER_H */
+
+/* ex:set ts=8 noet: */
diff --git a/gspell/gspell-text-view.c b/gspell/gspell-text-view.c
new file mode 100644
index 0000000..d5a0bb0
--- /dev/null
+++ b/gspell/gspell-text-view.c
@@ -0,0 +1,615 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2015, 2016, 2017 - Sébastien Wilmet
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "gspell-text-view.h"
+#include <glib/gi18n-lib.h>
+#include "gspell-inline-checker-text-buffer.h"
+#include "gspell-checker.h"
+#include "gspell-language.h"
+#include "gspell-text-buffer.h"
+#include "gspell-context-menu.h"
+
+/**
+ * SECTION:text-view
+ * @Title: GspellTextView
+ * @Short_description: Spell checking support for GtkTextView
+ *
+ * #GspellTextView extends the #GtkTextView class with inline spell checking.
+ * Misspelled words are highlighted with a red %PANGO_UNDERLINE_SINGLE.
+ * Right-clicking a misspelled word pops up a context menu of suggested
+ * replacements. The context menu also contains an “Ignore All” item to add the
+ * misspelled word to the session dictionary. And an “Add” item to add the word
+ * to the personal dictionary.
+ *
+ * For a basic use-case, there is the gspell_text_view_basic_setup() convenience
+ * function.
+ *
+ * The spell is checked only on the visible region of the #GtkTextView. Note
+ * that if a same #GtkTextBuffer is used for several views, the misspelled words
+ * are visible in all views, because the highlighting is achieved with a
+ * #GtkTextTag added to the buffer.
+ *
+ * If you don't use the gspell_text_view_basic_setup() function, you need to
+ * call gspell_text_buffer_set_spell_checker() to associate a #GspellChecker to
+ * the #GtkTextBuffer.
+ *
+ * Note that #GspellTextView extends the #GtkTextView class but without
+ * subclassing it, because the GtkSourceView library has already a #GtkTextView
+ * subclass.
+ *
+ * If you want a %PANGO_UNDERLINE_ERROR instead (a wavy underline), please fix
+ * [this bug](https://bugzilla.gnome.org/show_bug.cgi?id=763741) first.
+ */
+
+typedef struct _GspellTextViewPrivate GspellTextViewPrivate;
+
+struct _GspellTextViewPrivate
+{
+ GtkTextView *view;
+ GspellInlineCheckerTextBuffer *inline_checker;
+ guint enable_language_menu : 1;
+};
+
+enum
+{
+ PROP_0,
+ PROP_VIEW,
+ PROP_INLINE_SPELL_CHECKING,
+ PROP_ENABLE_LANGUAGE_MENU,
+};
+
+#define GSPELL_TEXT_VIEW_KEY "gspell-text-view-key"
+
+G_DEFINE_TYPE_WITH_PRIVATE (GspellTextView, gspell_text_view, G_TYPE_OBJECT)
+
+static void
+create_inline_checker (GspellTextView *gspell_view)
+{
+ GspellTextViewPrivate *priv;
+ GtkTextBuffer *buffer;
+
+ priv = gspell_text_view_get_instance_private (gspell_view);
+
+ if (priv->inline_checker != NULL)
+ {
+ return;
+ }
+
+ buffer = gtk_text_view_get_buffer (priv->view);
+ priv->inline_checker = _gspell_inline_checker_text_buffer_new (buffer);
+ _gspell_inline_checker_text_buffer_attach_view (priv->inline_checker,
+ priv->view);
+}
+
+static void
+destroy_inline_checker (GspellTextView *gspell_view)
+{
+ GspellTextViewPrivate *priv;
+
+ priv = gspell_text_view_get_instance_private (gspell_view);
+
+ if (priv->view == NULL || priv->inline_checker == NULL)
+ {
+ return;
+ }
+
+ _gspell_inline_checker_text_buffer_detach_view (priv->inline_checker,
+ priv->view);
+ g_clear_object (&priv->inline_checker);
+}
+
+static void
+notify_buffer_cb (GtkTextView *gtk_view,
+ GParamSpec *pspec,
+ GspellTextView *gspell_view)
+{
+ GspellTextViewPrivate *priv;
+
+ priv = gspell_text_view_get_instance_private (gspell_view);
+
+ if (priv->inline_checker == NULL)
+ {
+ return;
+ }
+
+ destroy_inline_checker (gspell_view);
+ create_inline_checker (gspell_view);
+}
+
+static void
+language_activated_cb (const GspellLanguage *lang,
+ gpointer user_data)
+{
+ GspellTextView *gspell_view;
+ GspellTextViewPrivate *priv;
+ GtkTextBuffer *gtk_buffer;
+ GspellTextBuffer *gspell_buffer;
+ GspellChecker *checker;
+
+ g_return_if_fail (GSPELL_IS_TEXT_VIEW (user_data));
+
+ gspell_view = GSPELL_TEXT_VIEW (user_data);
+ priv = gspell_text_view_get_instance_private (gspell_view);
+
+ gtk_buffer = gtk_text_view_get_buffer (priv->view);
+ gspell_buffer = gspell_text_buffer_get_from_gtk_text_buffer (gtk_buffer);
+ checker = gspell_text_buffer_get_spell_checker (gspell_buffer);
+
+ gspell_checker_set_language (checker, lang);
+}
+
+static const GspellLanguage *
+get_current_language (GspellTextView *gspell_view)
+{
+ GspellTextViewPrivate *priv;
+ GtkTextBuffer *gtk_buffer;
+ GspellTextBuffer *gspell_buffer;
+ GspellChecker *checker;
+
+ priv = gspell_text_view_get_instance_private (gspell_view);
+
+ if (priv->view == NULL)
+ {
+ return NULL;
+ }
+
+ gtk_buffer = gtk_text_view_get_buffer (priv->view);
+ gspell_buffer = gspell_text_buffer_get_from_gtk_text_buffer (gtk_buffer);
+ checker = gspell_text_buffer_get_spell_checker (gspell_buffer);
+
+ return gspell_checker_get_language (checker);
+}
+
+static void
+populate_popup_cb (GtkTextView *gtk_view,
+ GtkWidget *popup,
+ GspellTextView *gspell_view)
+{
+ GspellTextViewPrivate *priv;
+ GtkMenu *menu;
+ GtkWidget *menu_item;
+
+ priv = gspell_text_view_get_instance_private (gspell_view);
+
+ if (!GTK_IS_MENU (popup))
+ {
+ return;
+ }
+
+ menu = GTK_MENU (popup);
+
+ if (!priv->enable_language_menu &&
+ priv->inline_checker == NULL)
+ {
+ return;
+ }
+
+ /* Prepend separator */
+ menu_item = gtk_separator_menu_item_new ();
+ gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), menu_item);
+ gtk_widget_show (menu_item);
+
+ if (priv->enable_language_menu)
+ {
+ const GspellLanguage *current_language;
+ GtkMenuItem *lang_menu_item;
+
+ current_language = get_current_language (gspell_view);
+ lang_menu_item = _gspell_context_menu_get_language_menu_item (current_language,
+ language_activated_cb,
+ gspell_view);
+
+ /* Prepend language sub-menu */
+ gtk_menu_shell_prepend (GTK_MENU_SHELL (menu),
+ GTK_WIDGET (lang_menu_item));
+ }
+
+ if (priv->inline_checker != NULL)
+ {
+ /* Prepend suggestions */
+ _gspell_inline_checker_text_buffer_populate_popup (priv->inline_checker, menu);
+ }
+}
+
+static void
+set_view (GspellTextView *gspell_view,
+ GtkTextView *gtk_view)
+{
+ GspellTextViewPrivate *priv;
+
+ g_return_if_fail (GTK_IS_TEXT_VIEW (gtk_view));
+
+ priv = gspell_text_view_get_instance_private (gspell_view);
+
+ g_assert (priv->view == NULL);
+ g_assert (priv->inline_checker == NULL);
+
+ priv->view = gtk_view;
+
+ g_signal_connect_object (priv->view,
+ "notify::buffer",
+ G_CALLBACK (notify_buffer_cb),
+ gspell_view,
+ 0);
+
+ /* G_CONNECT_AFTER, so when menu items are prepended, they have more
+ * chances to be the first in the menu.
+ */
+ g_signal_connect_object (priv->view,
+ "populate-popup",
+ G_CALLBACK (populate_popup_cb),
+ gspell_view,
+ G_CONNECT_AFTER);
+
+ g_object_notify (G_OBJECT (gspell_view), "view");
+}
+
+static void
+gspell_text_view_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GspellTextView *gspell_view = GSPELL_TEXT_VIEW (object);
+
+ switch (prop_id)
+ {
+ case PROP_VIEW:
+ g_value_set_object (value, gspell_text_view_get_view (gspell_view));
+ break;
+
+ case PROP_INLINE_SPELL_CHECKING:
+ g_value_set_boolean (value, gspell_text_view_get_inline_spell_checking (gspell_view));
+ break;
+
+ case PROP_ENABLE_LANGUAGE_MENU:
+ g_value_set_boolean (value, gspell_text_view_get_enable_language_menu (gspell_view));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gspell_text_view_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GspellTextView *gspell_view = GSPELL_TEXT_VIEW (object);
+
+ switch (prop_id)
+ {
+ case PROP_VIEW:
+ set_view (gspell_view, g_value_get_object (value));
+ break;
+
+ case PROP_INLINE_SPELL_CHECKING:
+ gspell_text_view_set_inline_spell_checking (gspell_view, g_value_get_boolean (value));
+ break;
+
+ case PROP_ENABLE_LANGUAGE_MENU:
+ gspell_text_view_set_enable_language_menu (gspell_view, g_value_get_boolean (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gspell_text_view_dispose (GObject *object)
+{
+ GspellTextViewPrivate *priv;
+
+ priv = gspell_text_view_get_instance_private (GSPELL_TEXT_VIEW (object));
+
+ if (priv->view != NULL && priv->inline_checker != NULL)
+ {
+ _gspell_inline_checker_text_buffer_detach_view (priv->inline_checker,
+ priv->view);
+ }
+
+ priv->view = NULL;
+ g_clear_object (&priv->inline_checker);
+
+ G_OBJECT_CLASS (gspell_text_view_parent_class)->dispose (object);
+}
+
+static void
+gspell_text_view_class_init (GspellTextViewClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->get_property = gspell_text_view_get_property;
+ object_class->set_property = gspell_text_view_set_property;
+ object_class->dispose = gspell_text_view_dispose;
+
+ /**
+ * GspellTextView:view:
+ *
+ * The #GtkTextView.
+ */
+ g_object_class_install_property (object_class,
+ PROP_VIEW,
+ g_param_spec_object ("view",
+ "View",
+ "",
+ GTK_TYPE_TEXT_VIEW,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * GspellTextView:inline-spell-checking:
+ *
+ * Whether the inline spell checking is enabled.
+ */
+ g_object_class_install_property (object_class,
+ PROP_INLINE_SPELL_CHECKING,
+ g_param_spec_boolean ("inline-spell-checking",
+ "Inline Spell Checking",
+ "",
+ FALSE,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * GspellTextView:enable-language-menu:
+ *
+ * When the context menu is shown, whether to add a sub-menu to select
+ * the language for the spell checking.
+ *
+ * Since: 1.2
+ */
+ g_object_class_install_property (object_class,
+ PROP_ENABLE_LANGUAGE_MENU,
+ g_param_spec_boolean ("enable-language-menu",
+ "Enable Language Menu",
+ "",
+ FALSE,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+}
+
+static void
+gspell_text_view_init (GspellTextView *gspell_view)
+{
+}
+
+/**
+ * gspell_text_view_get_from_gtk_text_view:
+ * @gtk_view: a #GtkTextView.
+ *
+ * Returns the #GspellTextView of @gtk_view. The returned object is guaranteed
+ * to be the same for the lifetime of @gtk_view.
+ *
+ * Returns: (transfer none): the #GspellTextView of @gtk_view.
+ */
+GspellTextView *
+gspell_text_view_get_from_gtk_text_view (GtkTextView *gtk_view)
+{
+ GspellTextView *gspell_view;
+
+ g_return_val_if_fail (GTK_IS_TEXT_VIEW (gtk_view), NULL);
+
+ gspell_view = g_object_get_data (G_OBJECT (gtk_view), GSPELL_TEXT_VIEW_KEY);
+
+ if (gspell_view == NULL)
+ {
+ gspell_view = g_object_new (GSPELL_TYPE_TEXT_VIEW,
+ "view", gtk_view,
+ NULL);
+
+ g_object_set_data_full (G_OBJECT (gtk_view),
+ GSPELL_TEXT_VIEW_KEY,
+ gspell_view,
+ g_object_unref);
+ }
+
+ g_return_val_if_fail (GSPELL_IS_TEXT_VIEW (gspell_view), NULL);
+ return gspell_view;
+}
+
+/**
+ * gspell_text_view_basic_setup:
+ * @gspell_view: a #GspellTextView.
+ *
+ * This function is a convenience function that does the following:
+ * - Set a spell checker. The language chosen is the one returned by
+ * gspell_language_get_default().
+ * - Set the #GspellTextView:inline-spell-checking property to %TRUE.
+ * - Set the #GspellTextView:enable-language-menu property to %TRUE.
+ *
+ * Example:
+ * |[
+ * GtkTextView *gtk_view;
+ * GspellTextView *gspell_view;
+ *
+ * gspell_view = gspell_text_view_get_from_gtk_text_view (gtk_view);
+ * gspell_text_view_basic_setup (gspell_view);
+ * ]|
+ *
+ * This is equivalent to:
+ * |[
+ * GtkTextView *gtk_view;
+ * GspellTextView *gspell_view;
+ * GspellChecker *checker;
+ * GtkTextBuffer *gtk_buffer;
+ * GspellTextBuffer *gspell_buffer;
+ *
+ * checker = gspell_checker_new (NULL);
+ * gtk_buffer = gtk_text_view_get_buffer (gtk_view);
+ * gspell_buffer = gspell_text_buffer_get_from_gtk_text_buffer (gtk_buffer);
+ * gspell_text_buffer_set_spell_checker (gspell_buffer, checker);
+ * g_object_unref (checker);
+ *
+ * gspell_view = gspell_text_view_get_from_gtk_text_view (gtk_view);
+ * gspell_text_view_set_inline_spell_checking (gspell_view, TRUE);
+ * gspell_text_view_set_enable_language_menu (gspell_view, TRUE);
+ * ]|
+ *
+ * Since: 1.2
+ */
+void
+gspell_text_view_basic_setup (GspellTextView *gspell_view)
+{
+ GspellTextViewPrivate *priv;
+ GspellChecker *checker;
+ GtkTextBuffer *gtk_buffer;
+ GspellTextBuffer *gspell_buffer;
+
+ g_return_if_fail (GSPELL_IS_TEXT_VIEW (gspell_view));
+
+ priv = gspell_text_view_get_instance_private (gspell_view);
+
+ checker = gspell_checker_new (NULL);
+ gtk_buffer = gtk_text_view_get_buffer (priv->view);
+ gspell_buffer = gspell_text_buffer_get_from_gtk_text_buffer (gtk_buffer);
+ gspell_text_buffer_set_spell_checker (gspell_buffer, checker);
+ g_object_unref (checker);
+
+ gspell_text_view_set_inline_spell_checking (gspell_view, TRUE);
+ gspell_text_view_set_enable_language_menu (gspell_view, TRUE);
+}
+
+/**
+ * gspell_text_view_get_view:
+ * @gspell_view: a #GspellTextView.
+ *
+ * Returns: (transfer none): the #GtkTextView of @gspell_view.
+ */
+GtkTextView *
+gspell_text_view_get_view (GspellTextView *gspell_view)
+{
+ GspellTextViewPrivate *priv;
+
+ g_return_val_if_fail (GSPELL_IS_TEXT_VIEW (gspell_view), NULL);
+
+ priv = gspell_text_view_get_instance_private (gspell_view);
+ return priv->view;
+}
+
+/**
+ * gspell_text_view_get_inline_spell_checking:
+ * @gspell_view: a #GspellTextView.
+ *
+ * Returns: whether the inline spell checking is enabled.
+ */
+gboolean
+gspell_text_view_get_inline_spell_checking (GspellTextView *gspell_view)
+{
+ GspellTextViewPrivate *priv;
+
+ g_return_val_if_fail (GSPELL_IS_TEXT_VIEW (gspell_view), FALSE);
+
+ priv = gspell_text_view_get_instance_private (gspell_view);
+ return priv->inline_checker != NULL;
+}
+
+/**
+ * gspell_text_view_set_inline_spell_checking:
+ * @gspell_view: a #GspellTextView.
+ * @enable: the new state.
+ *
+ * Enables or disables the inline spell checking.
+ */
+void
+gspell_text_view_set_inline_spell_checking (GspellTextView *gspell_view,
+ gboolean enable)
+{
+ g_return_if_fail (GSPELL_IS_TEXT_VIEW (gspell_view));
+
+ enable = enable != FALSE;
+
+ if (enable == gspell_text_view_get_inline_spell_checking (gspell_view))
+ {
+ return;
+ }
+
+ if (enable)
+ {
+ create_inline_checker (gspell_view);
+ }
+ else
+ {
+ destroy_inline_checker (gspell_view);
+ }
+
+ g_object_notify (G_OBJECT (gspell_view), "inline-spell-checking");
+}
+
+/**
+ * gspell_text_view_get_enable_language_menu:
+ * @gspell_view: a #GspellTextView.
+ *
+ * Returns: whether the language context menu is enabled.
+ * Since: 1.2
+ */
+gboolean
+gspell_text_view_get_enable_language_menu (GspellTextView *gspell_view)
+{
+ GspellTextViewPrivate *priv;
+
+ g_return_val_if_fail (GSPELL_IS_TEXT_VIEW (gspell_view), FALSE);
+
+ priv = gspell_text_view_get_instance_private (gspell_view);
+ return priv->enable_language_menu;
+}
+
+/**
+ * gspell_text_view_set_enable_language_menu:
+ * @gspell_view: a #GspellTextView.
+ * @enable_language_menu: whether to enable the language context menu.
+ *
+ * Sets whether to enable the language context menu. If enabled, doing a right
+ * click on the #GtkTextView will show a sub-menu to choose the language for the
+ * spell checking. If another language is chosen, it changes the
+ * #GspellChecker:language property of the #GspellTextBuffer:spell-checker of
+ * the #GtkTextView:buffer of the #GspellTextView:view.
+ *
+ * Since: 1.2
+ */
+void
+gspell_text_view_set_enable_language_menu (GspellTextView *gspell_view,
+ gboolean enable_language_menu)
+{
+ GspellTextViewPrivate *priv;
+
+ g_return_if_fail (GSPELL_IS_TEXT_VIEW (gspell_view));
+
+ priv = gspell_text_view_get_instance_private (gspell_view);
+
+ enable_language_menu = enable_language_menu != FALSE;
+
+ if (priv->enable_language_menu != enable_language_menu)
+ {
+ priv->enable_language_menu = enable_language_menu;
+ g_object_notify (G_OBJECT (gspell_view), "enable-language-menu");
+ }
+}
+
+/* ex:set ts=8 noet: */
diff --git a/gspell/gspell-text-view.h b/gspell/gspell-text-view.h
new file mode 100644
index 0000000..371f47c
--- /dev/null
+++ b/gspell/gspell-text-view.h
@@ -0,0 +1,74 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2015, 2016 - Sébastien Wilmet
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GSPELL_TEXT_VIEW_H
+#define GSPELL_TEXT_VIEW_H
+
+#if !defined (GSPELL_H_INSIDE) && !defined (GSPELL_COMPILATION)
+#error "Only <gspell/gspell.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+#include <gspell/gspell-version.h>
+
+G_BEGIN_DECLS
+
+#define GSPELL_TYPE_TEXT_VIEW (gspell_text_view_get_type ())
+
+GSPELL_AVAILABLE_IN_ALL
+G_DECLARE_DERIVABLE_TYPE (GspellTextView, gspell_text_view,
+ GSPELL, TEXT_VIEW,
+ GObject)
+
+struct _GspellTextViewClass
+{
+ GObjectClass parent_class;
+
+ /* Padding for future expansion */
+ gpointer padding[8];
+};
+
+GSPELL_AVAILABLE_IN_ALL
+GspellTextView * gspell_text_view_get_from_gtk_text_view (GtkTextView *gtk_view);
+
+GSPELL_AVAILABLE_IN_1_2
+void gspell_text_view_basic_setup (GspellTextView *gspell_view);
+
+GSPELL_AVAILABLE_IN_ALL
+GtkTextView * gspell_text_view_get_view (GspellTextView *gspell_view);
+
+GSPELL_AVAILABLE_IN_ALL
+gboolean gspell_text_view_get_inline_spell_checking (GspellTextView *gspell_view);
+
+GSPELL_AVAILABLE_IN_ALL
+void gspell_text_view_set_inline_spell_checking (GspellTextView *gspell_view,
+ gboolean enable);
+
+GSPELL_AVAILABLE_IN_1_2
+gboolean gspell_text_view_get_enable_language_menu (GspellTextView *gspell_view);
+
+GSPELL_AVAILABLE_IN_1_2
+void gspell_text_view_set_enable_language_menu (GspellTextView *gspell_view,
+ gboolean
enable_language_menu);
+
+G_END_DECLS
+
+#endif /* GSPELL_TEXT_VIEW_H */
+
+/* ex:set ts=8 noet: */
diff --git a/gspell/gspell-utils.c b/gspell/gspell-utils.c
new file mode 100644
index 0000000..4bc8259
--- /dev/null
+++ b/gspell/gspell-utils.c
@@ -0,0 +1,284 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2010 - Jesse van den Kieboom
+ * Copyright 2015, 2016, 2017 - Sébastien Wilmet
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "gspell-utils.h"
+#include <string.h>
+#include "gspell-text-iter.h"
+
+gboolean
+_gspell_utils_is_number (const gchar *text,
+ gssize text_length)
+{
+ const gchar *p;
+ const gchar *end;
+
+ g_return_val_if_fail (text != NULL, FALSE);
+ g_return_val_if_fail (text_length >= -1, FALSE);
+
+ if (text_length == -1)
+ {
+ text_length = strlen (text);
+ }
+
+ p = text;
+ end = text + text_length;
+
+ while (p != NULL && *p != '\0')
+ {
+ gunichar c = g_utf8_get_char (p);
+
+ if (!g_unichar_isdigit (c) && c != '.' && c != ',')
+ {
+ return FALSE;
+ }
+
+ p = g_utf8_find_next_char (p, end);
+ }
+
+ return TRUE;
+}
+
+GtkTextTag *
+_gspell_utils_get_no_spell_check_tag (GtkTextBuffer *buffer)
+{
+ GtkTextTagTable *tag_table;
+
+ g_return_val_if_fail (GTK_IS_TEXT_BUFFER (buffer), NULL);
+
+ tag_table = gtk_text_buffer_get_tag_table (buffer);
+
+ return gtk_text_tag_table_lookup (tag_table, "gtksourceview:context-classes:no-spell-check");
+}
+
+gboolean
+_gspell_utils_skip_no_spell_check (GtkTextTag *no_spell_check_tag,
+ GtkTextIter *start,
+ const GtkTextIter *end)
+{
+ g_return_val_if_fail (start != NULL, FALSE);
+ g_return_val_if_fail (end != NULL, FALSE);
+
+ if (no_spell_check_tag == NULL)
+ {
+ return TRUE;
+ }
+
+ g_return_val_if_fail (GTK_IS_TEXT_TAG (no_spell_check_tag), FALSE);
+
+ while (gtk_text_iter_has_tag (start, no_spell_check_tag))
+ {
+ GtkTextIter last = *start;
+
+ if (!gtk_text_iter_forward_to_tag_toggle (start, no_spell_check_tag))
+ {
+ return FALSE;
+ }
+
+ if (gtk_text_iter_compare (start, &last) <= 0)
+ {
+ return FALSE;
+ }
+
+ _gspell_text_iter_forward_word_end (start);
+ _gspell_text_iter_backward_word_start (start);
+
+ if (gtk_text_iter_compare (start, &last) <= 0)
+ {
+ return FALSE;
+ }
+
+ if (gtk_text_iter_compare (start, end) >= 0)
+ {
+ return FALSE;
+ }
+ }
+
+ return TRUE;
+}
+
+/**
+ * _gspell_utils_str_replace:
+ * @string: a string
+ * @search: the search string
+ * @replacement: the replacement string
+ *
+ * Replaces all occurences of @search by @replacement.
+ *
+ * Returns: A newly allocated string with the replacements. Free with g_free().
+ */
+gchar *
+_gspell_utils_str_replace (const gchar *string,
+ const gchar *search,
+ const gchar *replacement)
+{
+ gchar **chunks;
+ gchar *ret;
+
+ g_return_val_if_fail (string != NULL, NULL);
+ g_return_val_if_fail (search != NULL, NULL);
+ g_return_val_if_fail (replacement != NULL, NULL);
+
+ chunks = g_strsplit (string, search, -1);
+ if (chunks != NULL && chunks[0] != NULL)
+ {
+ ret = g_strjoinv (replacement, chunks);
+ }
+ else
+ {
+ ret = g_strdup (string);
+ }
+
+ g_strfreev (chunks);
+ return ret;
+}
+
+/* Replaces unicode (non-ascii) apostrophes by the ascii apostrophe.
+ * Because with unicode apostrophes, the word is marked as misspelled. It should
+ * probably be fixed in hunspell, aspell, etc.
+ * Returns: %TRUE if @result has been set, %FALSE if @word must be used
+ * (to avoid a malloc).
+ */
+gboolean
+_gspell_utils_str_to_ascii_apostrophe (const gchar *word,
+ gssize word_length,
+ gchar **result)
+{
+ gchar *word_to_free = NULL;
+ const gchar *nul_terminated_word;
+
+ g_return_val_if_fail (word != NULL, FALSE);
+ g_return_val_if_fail (word_length >= -1, FALSE);
+ g_return_val_if_fail (result != NULL, FALSE);
+
+ if (g_utf8_strchr (word, word_length, _GSPELL_MODIFIER_LETTER_APOSTROPHE) == NULL &&
+ g_utf8_strchr (word, word_length, _GSPELL_RIGHT_SINGLE_QUOTATION_MARK) == NULL)
+ {
+ return FALSE;
+ }
+
+ if (word_length == -1)
+ {
+ nul_terminated_word = word;
+ }
+ else
+ {
+ word_to_free = g_strndup (word, word_length);
+ nul_terminated_word = word_to_free;
+ }
+
+ *result = _gspell_utils_str_replace (nul_terminated_word, "\xCA\xBC", "'");
+
+ g_free (word_to_free);
+ word_to_free = *result;
+ *result = _gspell_utils_str_replace (*result, "\xE2\x80\x99", "'");
+
+ g_free (word_to_free);
+ return TRUE;
+}
+
+gboolean
+_gspell_utils_is_apostrophe_or_dash (gunichar ch)
+{
+ return (ch == '-' ||
+ ch == '\'' ||
+ ch == _GSPELL_MODIFIER_LETTER_APOSTROPHE ||
+ ch == _GSPELL_RIGHT_SINGLE_QUOTATION_MARK);
+}
+
+/* Not the full intensity for the red, it's more readable with the red a bit
+ * darker for PANGO_UNDERLINE_SINGLE.
+ * For PANGO_UNDERLINE_ERROR, the full red intensity was used.
+ */
+#define UNDERLINE_COLOR_RED_INTENSITY (0.8)
+
+void
+_gspell_utils_init_underline_rgba (GdkRGBA *underline_color)
+{
+ g_return_if_fail (underline_color != NULL);
+
+ underline_color->red = UNDERLINE_COLOR_RED_INTENSITY;
+ underline_color->green = 0.0;
+ underline_color->blue = 0.0;
+ underline_color->alpha = 1.0;
+}
+
+PangoAttribute *
+_gspell_utils_create_pango_attr_underline_color (void)
+{
+ return pango_attr_underline_color_new (65535 * UNDERLINE_COLOR_RED_INTENSITY, 0, 0);
+}
+
+void
+_gspell_utils_improve_word_boundaries (const gchar *text,
+ PangoLogAttr *log_attrs,
+ gint n_attrs)
+{
+ const gchar *cur_text_pos;
+ gint attr_num;
+
+ attr_num = 0;
+ cur_text_pos = text;
+
+ while (attr_num < n_attrs)
+ {
+ PangoLogAttr *log_attr_before;
+ gunichar ch;
+ PangoLogAttr *log_attr_after;
+
+ if (cur_text_pos == NULL ||
+ *cur_text_pos == '\0')
+ {
+ if (attr_num != n_attrs - 1)
+ {
+ g_warning ("%s(): problem in loop iteration, attr_num=%d but should be %d.",
+ G_STRFUNC,
+ attr_num,
+ n_attrs - 1);
+ }
+
+ break;
+ }
+
+ g_assert_cmpint (attr_num + 1, <, n_attrs);
+
+ /* ch is between log_attr_before and log_attr_after. */
+ log_attr_before = log_attrs + attr_num;
+ ch = g_utf8_get_char (cur_text_pos);
+ log_attr_after = log_attr_before + 1;
+
+ /* Same algo as in gspell-text-iter.c. */
+ if (_gspell_utils_is_apostrophe_or_dash (ch) &&
+ log_attr_before->is_word_end &&
+ log_attr_after->is_word_start)
+ {
+ log_attr_before->is_word_end = FALSE;
+ log_attr_after->is_word_start = FALSE;
+ }
+
+ attr_num++;
+ cur_text_pos = g_utf8_find_next_char (cur_text_pos, NULL);
+ }
+}
+
+/* ex:set ts=8 noet: */
diff --git a/gspell/gspell-utils.h b/gspell/gspell-utils.h
new file mode 100644
index 0000000..7e02994
--- /dev/null
+++ b/gspell/gspell-utils.h
@@ -0,0 +1,72 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2010 - Jesse van den Kieboom
+ * Copyright 2015, 2016, 2017 - Sébastien Wilmet
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GSPELL_UTILS_H
+#define GSPELL_UTILS_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+/* gunichar decimal value of unicode apostrophe characters. */
+#define _GSPELL_MODIFIER_LETTER_APOSTROPHE (700) /* U+02BC */
+#define _GSPELL_RIGHT_SINGLE_QUOTATION_MARK (8217) /* U+2019 */
+
+G_GNUC_INTERNAL
+gboolean _gspell_utils_is_number (const gchar *text,
+ gssize text_length);
+
+G_GNUC_INTERNAL
+GtkTextTag * _gspell_utils_get_no_spell_check_tag (GtkTextBuffer *buffer);
+
+G_GNUC_INTERNAL
+gboolean _gspell_utils_skip_no_spell_check (GtkTextTag *no_spell_check_tag,
+ GtkTextIter *start,
+ const GtkTextIter *end);
+
+G_GNUC_INTERNAL
+gchar * _gspell_utils_str_replace (const gchar *string,
+ const gchar *search,
+ const gchar *replacement);
+
+G_GNUC_INTERNAL
+gboolean _gspell_utils_str_to_ascii_apostrophe (const gchar *word,
+ gssize word_length,
+ gchar **result);
+
+G_GNUC_INTERNAL
+gboolean _gspell_utils_is_apostrophe_or_dash (gunichar ch);
+
+G_GNUC_INTERNAL
+void _gspell_utils_init_underline_rgba (GdkRGBA *underline_color);
+
+G_GNUC_INTERNAL
+PangoAttribute *_gspell_utils_create_pango_attr_underline_color (void);
+
+G_GNUC_INTERNAL
+void _gspell_utils_improve_word_boundaries (const gchar *text,
+ PangoLogAttr *log_attrs,
+ gint n_attrs);
+
+G_END_DECLS
+
+#endif /* GSPELL_UTILS_H */
+
+/* ex:set ts=8 noet: */
diff --git a/gspell/gspell-version.h b/gspell/gspell-version.h
new file mode 100644
index 0000000..6709177
--- /dev/null
+++ b/gspell/gspell-version.h
@@ -0,0 +1,44 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2017 - Sébastien Wilmet
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GSPELL_VERSION_H
+#define GSPELL_VERSION_H
+
+#if !defined (GSPELL_H_INSIDE) && !defined (GSPELL_COMPILATION)
+#error "Only <gspell/gspell.h> can be included directly."
+#endif
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+#ifndef _GSPELL_EXTERN
+#define _GSPELL_EXTERN extern
+#endif
+
+#define GSPELL_AVAILABLE_IN_ALL _GSPELL_EXTERN
+#define GSPELL_AVAILABLE_IN_1_2 _GSPELL_EXTERN
+#define GSPELL_AVAILABLE_IN_1_4 _GSPELL_EXTERN
+#define GSPELL_AVAILABLE_IN_1_6 _GSPELL_EXTERN
+
+G_END_DECLS
+
+#endif /* GSPELL_VERSION_H */
+
+/* ex:set ts=8 noet: */
diff --git a/gspell/gspell.gresource.xml b/gspell/gspell.gresource.xml
new file mode 100644
index 0000000..18482eb
--- /dev/null
+++ b/gspell/gspell.gresource.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/org/gnome/gspell">
+ <file preprocess="xml-stripblanks">checker-dialog.ui</file>
+ <file preprocess="xml-stripblanks">language-dialog.ui</file>
+ </gresource>
+</gresources>
diff --git a/gspell/gspell.h b/gspell/gspell.h
new file mode 100644
index 0000000..e3dc719
--- /dev/null
+++ b/gspell/gspell.h
@@ -0,0 +1,45 @@
+/*
+ * This file is part of gspell, a spell-checking library.
+ *
+ * Copyright 2015 - Sébastien Wilmet <swilmet gnome org>
+ *
+ * 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.1 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GSPELL_H
+#define GSPELL_H
+
+#define GSPELL_H_INSIDE
+
+#include <gspell/gspell-checker.h>
+#include <gspell/gspell-checker-dialog.h>
+#include <gspell/gspell-entry.h>
+#include <gspell/gspell-entry-buffer.h>
+#include <gspell/gspell-language.h>
+#include <gspell/gspell-language-chooser.h>
+#include <gspell/gspell-language-chooser-button.h>
+#include <gspell/gspell-language-chooser-dialog.h>
+#include <gspell/gspell-navigator.h>
+#include <gspell/gspell-navigator-text-view.h>
+#include <gspell/gspell-text-buffer.h>
+#include <gspell/gspell-text-view.h>
+
+#include <gspell/gspell-enum-types.h>
+#include <gspell/gspell-version.h>
+
+#undef GSPELL_H_INSIDE
+
+#endif /* GSPELL_H */
+
+/* ex:set ts=8 noet: */
diff --git a/gspell/gspellregion.c b/gspell/gspellregion.c
new file mode 100644
index 0000000..a379ed7
--- /dev/null
+++ b/gspell/gspellregion.c
@@ -0,0 +1,1371 @@
+/* Do not edit: this file is generated from
https://git.gnome.org/browse/gtksourceview/plain/gtksourceview/gtksourceregion.c */
+
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * gspellregion.c - GtkTextMark-based region utility
+ * This file is part of GtkSourceView
+ *
+ * Copyright (C) 2002 Gustavo Giráldez <gustavo giraldez gmx net>
+ * Copyright (C) 2016 Sébastien Wilmet <swilmet gnome org>
+ *
+ * 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.1 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 Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "gspellregion.h"
+
+/*
+ * SECTION:region
+ * @Short_description: Region utility
+ * @Title: GspellRegion
+ * @See_also: #GtkTextBuffer
+ *
+ * A #GspellRegion permits to store a group of subregions of a
+ * #GtkTextBuffer. #GspellRegion stores the subregions with pairs of
+ * #GtkTextMark's, so the region is still valid after insertions and deletions
+ * in the #GtkTextBuffer.
+ *
+ * The #GtkTextMark for the start of a subregion has a left gravity, while the
+ * #GtkTextMark for the end of a subregion has a right gravity.
+ *
+ * The typical use-case of #GspellRegion is to scan a #GtkTextBuffer chunk by
+ * chunk, not the whole buffer at once to not block the user interface. The
+ * #GspellRegion represents in that case the remaining region to scan. You
+ * can listen to the #GtkTextBuffer::insert-text and
+ * #GtkTextBuffer::delete-range signals to update the #GspellRegion
+ * accordingly.
+ *
+ * To iterate through the subregions, you need to use a #GspellRegionIter,
+ * for example:
+ * |[
+ * GspellRegion *region;
+ * GspellRegionIter region_iter;
+ *
+ * _gspell_region_get_start_region_iter (region, ®ion_iter);
+ *
+ * while (!_gspell_region_iter_is_end (®ion_iter))
+ * {
+ * GtkTextIter subregion_start;
+ * GtkTextIter subregion_end;
+ *
+ * if (!_gspell_region_iter_get_subregion (®ion_iter,
+ * &subregion_start,
+ * &subregion_end))
+ * {
+ * break;
+ * }
+ *
+ * // Do something useful with the subregion.
+ *
+ * _gspell_region_iter_next (®ion_iter);
+ * }
+ * ]|
+ */
+
+/* With the gravities of the GtkTextMarks, it is possible for subregions to
+ * become interlaced:
+ * Buffer content:
+ * "hello world"
+ * Add two subregions:
+ * "[hello] [world]"
+ * Delete the space:
+ * "[hello][world]"
+ * Undo:
+ * "[hello[ ]world]"
+ *
+ * FIXME: when iterating through the subregions, it should simplify them first.
+ * I don't know if it's done (swilmet).
+ */
+
+#undef ENABLE_DEBUG
+/*
+#define ENABLE_DEBUG
+*/
+
+#ifdef ENABLE_DEBUG
+#define DEBUG(x) (x)
+#else
+#define DEBUG(x)
+#endif
+
+typedef struct _GspellRegionPrivate GspellRegionPrivate;
+typedef struct _Subregion Subregion;
+typedef struct _GspellRegionIterReal GspellRegionIterReal;
+
+struct _GspellRegionPrivate
+{
+ /* Weak pointer to the buffer. */
+ GtkTextBuffer *buffer;
+
+ /* List of sorted 'Subregion*' */
+ GList *subregions;
+
+ guint32 timestamp;
+};
+
+struct _Subregion
+{
+ GtkTextMark *start;
+ GtkTextMark *end;
+};
+
+struct _GspellRegionIterReal
+{
+ GspellRegion *region;
+ guint32 region_timestamp;
+ GList *subregions;
+};
+
+enum
+{
+ PROP_0,
+ PROP_BUFFER,
+ N_PROPERTIES
+};
+
+static GParamSpec *properties[N_PROPERTIES];
+
+G_DEFINE_TYPE_WITH_PRIVATE (GspellRegion, _gspell_region, G_TYPE_OBJECT)
+
+#ifdef ENABLE_DEBUG
+static void
+print_region (GspellRegion *region)
+{
+ gchar *str;
+
+ str = _gspell_region_to_string (region);
+ g_print ("%s\n", str);
+ g_free (str);
+}
+#endif
+
+/* Find and return a subregion node which contains the given text
+ * iter. If left_side is TRUE, return the subregion which contains
+ * the text iter or which is the leftmost; else return the rightmost
+ * subregion.
+ */
+static GList *
+find_nearest_subregion (GspellRegion *region,
+ const GtkTextIter *iter,
+ GList *begin,
+ gboolean leftmost,
+ gboolean include_edges)
+{
+ GspellRegionPrivate *priv = _gspell_region_get_instance_private (region);
+ GList *retval;
+ GList *l;
+
+ g_assert (iter != NULL);
+
+ if (begin == NULL)
+ {
+ begin = priv->subregions;
+ }
+
+ if (begin != NULL)
+ {
+ retval = begin->prev;
+ }
+ else
+ {
+ retval = NULL;
+ }
+
+ for (l = begin; l != NULL; l = l->next)
+ {
+ GtkTextIter sr_iter;
+ Subregion *sr = l->data;
+ gint cmp;
+
+ if (!leftmost)
+ {
+ gtk_text_buffer_get_iter_at_mark (priv->buffer, &sr_iter, sr->end);
+ cmp = gtk_text_iter_compare (iter, &sr_iter);
+ if (cmp < 0 || (cmp == 0 && include_edges))
+ {
+ retval = l;
+ break;
+ }
+
+ }
+ else
+ {
+ gtk_text_buffer_get_iter_at_mark (priv->buffer, &sr_iter, sr->start);
+ cmp = gtk_text_iter_compare (iter, &sr_iter);
+ if (cmp > 0 || (cmp == 0 && include_edges))
+ {
+ retval = l;
+ }
+ else
+ {
+ break;
+ }
+ }
+ }
+
+ return retval;
+}
+
+static void
+_gspell_region_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GspellRegion *region = GSPELL_REGION (object);
+
+ switch (prop_id)
+ {
+ case PROP_BUFFER:
+ g_value_set_object (value, _gspell_region_get_buffer (region));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+_gspell_region_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GspellRegionPrivate *priv = _gspell_region_get_instance_private (GSPELL_REGION (object));
+
+ switch (prop_id)
+ {
+ case PROP_BUFFER:
+ g_assert (priv->buffer == NULL);
+ priv->buffer = g_value_get_object (value);
+ g_object_add_weak_pointer (G_OBJECT (priv->buffer),
+ (gpointer *) &priv->buffer);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+_gspell_region_dispose (GObject *object)
+{
+ GspellRegionPrivate *priv = _gspell_region_get_instance_private (GSPELL_REGION (object));
+
+ while (priv->subregions != NULL)
+ {
+ Subregion *sr = priv->subregions->data;
+
+ if (priv->buffer != NULL)
+ {
+ gtk_text_buffer_delete_mark (priv->buffer, sr->start);
+ gtk_text_buffer_delete_mark (priv->buffer, sr->end);
+ }
+
+ g_slice_free (Subregion, sr);
+ priv->subregions = g_list_delete_link (priv->subregions, priv->subregions);
+ }
+
+ if (priv->buffer != NULL)
+ {
+ g_object_remove_weak_pointer (G_OBJECT (priv->buffer),
+ (gpointer *) &priv->buffer);
+
+ priv->buffer = NULL;
+ }
+
+ G_OBJECT_CLASS (_gspell_region_parent_class)->dispose (object);
+}
+
+static void
+_gspell_region_class_init (GspellRegionClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->get_property = _gspell_region_get_property;
+ object_class->set_property = _gspell_region_set_property;
+ object_class->dispose = _gspell_region_dispose;
+
+ /*
+ * GspellRegion:buffer:
+ *
+ * The #GtkTextBuffer. The #GspellRegion has a weak reference to the
+ * buffer.
+ *
+ * Since: 3.22
+ */
+ properties[PROP_BUFFER] =
+ g_param_spec_object ("buffer",
+ "Buffer",
+ "",
+ GTK_TYPE_TEXT_BUFFER,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class, N_PROPERTIES, properties);
+}
+
+static void
+_gspell_region_init (GspellRegion *region)
+{
+}
+
+/*
+ * _gspell_region_new:
+ * @buffer: a #GtkTextBuffer.
+ *
+ * Returns: a new #GspellRegion object for @buffer.
+ * Since: 3.22
+ */
+GspellRegion *
+_gspell_region_new (GtkTextBuffer *buffer)
+{
+ g_return_val_if_fail (GTK_IS_TEXT_BUFFER (buffer), NULL);
+
+ return g_object_new (GSPELL_TYPE_REGION,
+ "buffer", buffer,
+ NULL);
+}
+
+/*
+ * _gspell_region_get_buffer:
+ * @region: a #GspellRegion.
+ *
+ * Returns: (transfer none) (nullable): the #GtkTextBuffer.
+ * Since: 3.22
+ */
+GtkTextBuffer *
+_gspell_region_get_buffer (GspellRegion *region)
+{
+ GspellRegionPrivate *priv;
+
+ g_return_val_if_fail (GSPELL_IS_REGION (region), NULL);
+
+ priv = _gspell_region_get_instance_private (region);
+ return priv->buffer;
+}
+
+static void
+_gspell_region_clear_zero_length_subregions (GspellRegion *region)
+{
+ GspellRegionPrivate *priv = _gspell_region_get_instance_private (region);
+ GList *node;
+
+ node = priv->subregions;
+ while (node != NULL)
+ {
+ Subregion *sr = node->data;
+ GtkTextIter start;
+ GtkTextIter end;
+
+ gtk_text_buffer_get_iter_at_mark (priv->buffer, &start, sr->start);
+ gtk_text_buffer_get_iter_at_mark (priv->buffer, &end, sr->end);
+
+ if (gtk_text_iter_equal (&start, &end))
+ {
+ gtk_text_buffer_delete_mark (priv->buffer, sr->start);
+ gtk_text_buffer_delete_mark (priv->buffer, sr->end);
+ g_slice_free (Subregion, sr);
+
+ if (node == priv->subregions)
+ {
+ priv->subregions = node = g_list_delete_link (node, node);
+ }
+ else
+ {
+ node = g_list_delete_link (node, node);
+ }
+
+ priv->timestamp++;
+ }
+ else
+ {
+ node = node->next;
+ }
+ }
+}
+
+/*
+ * _gspell_region_add_subregion:
+ * @region: a #GspellRegion.
+ * @_start: the start of the subregion.
+ * @_end: the end of the subregion.
+ *
+ * Adds the subregion delimited by @_start and @_end to @region.
+ *
+ * Since: 3.22
+ */
+void
+_gspell_region_add_subregion (GspellRegion *region,
+ const GtkTextIter *_start,
+ const GtkTextIter *_end)
+{
+ GspellRegionPrivate *priv;
+ GList *start_node;
+ GList *end_node;
+ GtkTextIter start;
+ GtkTextIter end;
+
+ g_return_if_fail (GSPELL_IS_REGION (region));
+ g_return_if_fail (_start != NULL);
+ g_return_if_fail (_end != NULL);
+
+ priv = _gspell_region_get_instance_private (region);
+
+ if (priv->buffer == NULL)
+ {
+ return;
+ }
+
+ start = *_start;
+ end = *_end;
+
+ DEBUG (g_print ("---\n"));
+ DEBUG (print_region (region));
+ DEBUG (g_message ("region_add (%d, %d)",
+ gtk_text_iter_get_offset (&start),
+ gtk_text_iter_get_offset (&end)));
+
+ gtk_text_iter_order (&start, &end);
+
+ /* Don't add zero-length regions. */
+ if (gtk_text_iter_equal (&start, &end))
+ {
+ return;
+ }
+
+ /* Find bounding subregions. */
+ start_node = find_nearest_subregion (region, &start, NULL, FALSE, TRUE);
+ end_node = find_nearest_subregion (region, &end, start_node, TRUE, TRUE);
+
+ if (start_node == NULL || end_node == NULL || end_node == start_node->prev)
+ {
+ /* Create the new subregion. */
+ Subregion *sr = g_slice_new0 (Subregion);
+ sr->start = gtk_text_buffer_create_mark (priv->buffer, NULL, &start, TRUE);
+ sr->end = gtk_text_buffer_create_mark (priv->buffer, NULL, &end, FALSE);
+
+ if (start_node == NULL)
+ {
+ /* Append the new region. */
+ priv->subregions = g_list_append (priv->subregions, sr);
+ }
+ else if (end_node == NULL)
+ {
+ /* Prepend the new region. */
+ priv->subregions = g_list_prepend (priv->subregions, sr);
+ }
+ else
+ {
+ /* We are in the middle of two subregions. */
+ priv->subregions = g_list_insert_before (priv->subregions, start_node, sr);
+ }
+ }
+ else
+ {
+ GtkTextIter iter;
+ Subregion *sr = start_node->data;
+
+ if (start_node != end_node)
+ {
+ /* We need to merge some subregions. */
+ GList *l = start_node->next;
+ Subregion *q;
+
+ gtk_text_buffer_delete_mark (priv->buffer, sr->end);
+
+ while (l != end_node)
+ {
+ q = l->data;
+ gtk_text_buffer_delete_mark (priv->buffer, q->start);
+ gtk_text_buffer_delete_mark (priv->buffer, q->end);
+ g_slice_free (Subregion, q);
+ l = g_list_delete_link (l, l);
+ }
+
+ q = l->data;
+ gtk_text_buffer_delete_mark (priv->buffer, q->start);
+ sr->end = q->end;
+ g_slice_free (Subregion, q);
+ l = g_list_delete_link (l, l);
+ }
+
+ /* Now move marks if that action expands the region. */
+ gtk_text_buffer_get_iter_at_mark (priv->buffer, &iter, sr->start);
+ if (gtk_text_iter_compare (&iter, &start) > 0)
+ {
+ gtk_text_buffer_move_mark (priv->buffer, sr->start, &start);
+ }
+
+ gtk_text_buffer_get_iter_at_mark (priv->buffer, &iter, sr->end);
+ if (gtk_text_iter_compare (&iter, &end) < 0)
+ {
+ gtk_text_buffer_move_mark (priv->buffer, sr->end, &end);
+ }
+ }
+
+ priv->timestamp++;
+
+ DEBUG (print_region (region));
+}
+
+/*
+ * _gspell_region_add_region:
+ * @region: a #GspellRegion.
+ * @region_to_add: (nullable): the #GspellRegion to add to @region, or %NULL.
+ *
+ * Adds @region_to_add to @region. @region_to_add is not modified.
+ *
+ * Since: 3.22
+ */
+void
+_gspell_region_add_region (GspellRegion *region,
+ GspellRegion *region_to_add)
+{
+ GspellRegionIter iter;
+ GtkTextBuffer *region_buffer;
+ GtkTextBuffer *region_to_add_buffer;
+
+ g_return_if_fail (GSPELL_IS_REGION (region));
+ g_return_if_fail (region_to_add == NULL || GSPELL_IS_REGION (region_to_add));
+
+ if (region_to_add == NULL)
+ {
+ return;
+ }
+
+ region_buffer = _gspell_region_get_buffer (region);
+ region_to_add_buffer = _gspell_region_get_buffer (region_to_add);
+ g_return_if_fail (region_buffer == region_to_add_buffer);
+
+ if (region_buffer == NULL)
+ {
+ return;
+ }
+
+ _gspell_region_get_start_region_iter (region_to_add, &iter);
+
+ while (!_gspell_region_iter_is_end (&iter))
+ {
+ GtkTextIter subregion_start;
+ GtkTextIter subregion_end;
+
+ if (!_gspell_region_iter_get_subregion (&iter,
+ &subregion_start,
+ &subregion_end))
+ {
+ break;
+ }
+
+ _gspell_region_add_subregion (region,
+ &subregion_start,
+ &subregion_end);
+
+ _gspell_region_iter_next (&iter);
+ }
+}
+
+/*
+ * _gspell_region_subtract_subregion:
+ * @region: a #GspellRegion.
+ * @_start: the start of the subregion.
+ * @_end: the end of the subregion.
+ *
+ * Subtracts the subregion delimited by @_start and @_end from @region.
+ *
+ * Since: 3.22
+ */
+void
+_gspell_region_subtract_subregion (GspellRegion *region,
+ const GtkTextIter *_start,
+ const GtkTextIter *_end)
+{
+ GspellRegionPrivate *priv;
+ GList *start_node;
+ GList *end_node;
+ GList *node;
+ GtkTextIter sr_start_iter;
+ GtkTextIter sr_end_iter;
+ gboolean done;
+ gboolean start_is_outside;
+ gboolean end_is_outside;
+ Subregion *sr;
+ GtkTextIter start;
+ GtkTextIter end;
+
+ g_return_if_fail (GSPELL_IS_REGION (region));
+ g_return_if_fail (_start != NULL);
+ g_return_if_fail (_end != NULL);
+
+ priv = _gspell_region_get_instance_private (region);
+
+ if (priv->buffer == NULL)
+ {
+ return;
+ }
+
+ start = *_start;
+ end = *_end;
+
+ DEBUG (g_print ("---\n"));
+ DEBUG (print_region (region));
+ DEBUG (g_message ("region_substract (%d, %d)",
+ gtk_text_iter_get_offset (&start),
+ gtk_text_iter_get_offset (&end)));
+
+ gtk_text_iter_order (&start, &end);
+
+ /* Find bounding subregions. */
+ start_node = find_nearest_subregion (region, &start, NULL, FALSE, FALSE);
+ end_node = find_nearest_subregion (region, &end, start_node, TRUE, FALSE);
+
+ /* Easy case first. */
+ if (start_node == NULL || end_node == NULL || end_node == start_node->prev)
+ {
+ return;
+ }
+
+ /* Deal with the start point. */
+ start_is_outside = end_is_outside = FALSE;
+
+ sr = start_node->data;
+ gtk_text_buffer_get_iter_at_mark (priv->buffer, &sr_start_iter, sr->start);
+ gtk_text_buffer_get_iter_at_mark (priv->buffer, &sr_end_iter, sr->end);
+
+ if (gtk_text_iter_in_range (&start, &sr_start_iter, &sr_end_iter) &&
+ !gtk_text_iter_equal (&start, &sr_start_iter))
+ {
+ /* The starting point is inside the first subregion. */
+ if (gtk_text_iter_in_range (&end, &sr_start_iter, &sr_end_iter) &&
+ !gtk_text_iter_equal (&end, &sr_end_iter))
+ {
+ /* The ending point is also inside the first
+ * subregion: we need to split.
+ */
+ Subregion *new_sr = g_slice_new0 (Subregion);
+ new_sr->end = sr->end;
+ new_sr->start = gtk_text_buffer_create_mark (priv->buffer,
+ NULL,
+ &end,
+ TRUE);
+
+ start_node = g_list_insert_before (start_node, start_node->next, new_sr);
+
+ sr->end = gtk_text_buffer_create_mark (priv->buffer,
+ NULL,
+ &start,
+ FALSE);
+
+ /* No further processing needed. */
+ DEBUG (g_message ("subregion splitted"));
+
+ return;
+ }
+ else
+ {
+ /* The ending point is outside, so just move
+ * the end of the subregion to the starting point.
+ */
+ gtk_text_buffer_move_mark (priv->buffer, sr->end, &start);
+ }
+ }
+ else
+ {
+ /* The starting point is outside (and so to the left)
+ * of the first subregion.
+ */
+ DEBUG (g_message ("start is outside"));
+
+ start_is_outside = TRUE;
+ }
+
+ /* Deal with the end point. */
+ if (start_node != end_node)
+ {
+ sr = end_node->data;
+ gtk_text_buffer_get_iter_at_mark (priv->buffer, &sr_start_iter, sr->start);
+ gtk_text_buffer_get_iter_at_mark (priv->buffer, &sr_end_iter, sr->end);
+ }
+
+ if (gtk_text_iter_in_range (&end, &sr_start_iter, &sr_end_iter) &&
+ !gtk_text_iter_equal (&end, &sr_end_iter))
+ {
+ /* Ending point is inside, move the start mark. */
+ gtk_text_buffer_move_mark (priv->buffer, sr->start, &end);
+ }
+ else
+ {
+ end_is_outside = TRUE;
+ DEBUG (g_message ("end is outside"));
+ }
+
+ /* Finally remove any intermediate subregions. */
+ done = FALSE;
+ node = start_node;
+
+ while (!done)
+ {
+ if (node == end_node)
+ {
+ /* We are done, exit in the next iteration. */
+ done = TRUE;
+ }
+
+ if ((node == start_node && !start_is_outside) ||
+ (node == end_node && !end_is_outside))
+ {
+ /* Skip starting or ending node. */
+ node = node->next;
+ }
+ else
+ {
+ GList *l = node->next;
+ sr = node->data;
+ gtk_text_buffer_delete_mark (priv->buffer, sr->start);
+ gtk_text_buffer_delete_mark (priv->buffer, sr->end);
+ g_slice_free (Subregion, sr);
+ priv->subregions = g_list_delete_link (priv->subregions, node);
+ node = l;
+ }
+ }
+
+ priv->timestamp++;
+
+ DEBUG (print_region (region));
+
+ /* Now get rid of empty subregions. */
+ _gspell_region_clear_zero_length_subregions (region);
+
+ DEBUG (print_region (region));
+}
+
+/*
+ * _gspell_region_subtract_region:
+ * @region: a #GspellRegion.
+ * @region_to_subtract: (nullable): the #GspellRegion to subtract from
+ * @region, or %NULL.
+ *
+ * Subtracts @region_to_subtract from @region. @region_to_subtract is not
+ * modified.
+ *
+ * Since: 3.22
+ */
+void
+_gspell_region_subtract_region (GspellRegion *region,
+ GspellRegion *region_to_subtract)
+{
+ GtkTextBuffer *region_buffer;
+ GtkTextBuffer *region_to_subtract_buffer;
+ GspellRegionIter iter;
+
+ g_return_if_fail (GSPELL_IS_REGION (region));
+ g_return_if_fail (region_to_subtract == NULL || GSPELL_IS_REGION (region_to_subtract));
+
+ region_buffer = _gspell_region_get_buffer (region);
+ region_to_subtract_buffer = _gspell_region_get_buffer (region_to_subtract);
+ g_return_if_fail (region_buffer == region_to_subtract_buffer);
+
+ if (region_buffer == NULL)
+ {
+ return;
+ }
+
+ _gspell_region_get_start_region_iter (region_to_subtract, &iter);
+
+ while (!_gspell_region_iter_is_end (&iter))
+ {
+ GtkTextIter subregion_start;
+ GtkTextIter subregion_end;
+
+ if (!_gspell_region_iter_get_subregion (&iter,
+ &subregion_start,
+ &subregion_end))
+ {
+ break;
+ }
+
+ _gspell_region_subtract_subregion (region,
+ &subregion_start,
+ &subregion_end);
+
+ _gspell_region_iter_next (&iter);
+ }
+}
+
+/*
+ * _gspell_region_is_empty:
+ * @region: (nullable): a #GspellRegion, or %NULL.
+ *
+ * Returns whether the @region is empty. A %NULL @region is considered empty.
+ *
+ * Returns: whether the @region is empty.
+ * Since: 3.22
+ */
+gboolean
+_gspell_region_is_empty (GspellRegion *region)
+{
+ GspellRegionIter region_iter;
+
+ if (region == NULL)
+ {
+ return TRUE;
+ }
+
+ /* A #GspellRegion can contain empty subregions. So checking the
+ * number of subregions is not sufficient.
+ * When calling _gspell_region_add_subregion() with equal iters, the
+ * subregion is not added. But when a subregion becomes empty, due to
+ * text deletion, the subregion is not removed from the
+ * #GspellRegion.
+ */
+
+ _gspell_region_get_start_region_iter (region, ®ion_iter);
+
+ while (!_gspell_region_iter_is_end (®ion_iter))
+ {
+ GtkTextIter subregion_start;
+ GtkTextIter subregion_end;
+
+ if (!_gspell_region_iter_get_subregion (®ion_iter,
+ &subregion_start,
+ &subregion_end))
+ {
+ return TRUE;
+ }
+
+ if (!gtk_text_iter_equal (&subregion_start, &subregion_end))
+ {
+ return FALSE;
+ }
+
+ _gspell_region_iter_next (®ion_iter);
+ }
+
+ return TRUE;
+}
+
+/*
+ * _gspell_region_get_bounds:
+ * @region: a #GspellRegion.
+ * @start: (out) (optional): iterator to initialize with the start of @region,
+ * or %NULL.
+ * @end: (out) (optional): iterator to initialize with the end of @region,
+ * or %NULL.
+ *
+ * Gets the @start and @end bounds of the @region.
+ *
+ * Returns: %TRUE if @start and @end have been set successfully (if non-%NULL),
+ * or %FALSE if the @region is empty.
+ * Since: 3.22
+ */
+gboolean
+_gspell_region_get_bounds (GspellRegion *region,
+ GtkTextIter *start,
+ GtkTextIter *end)
+{
+ GspellRegionPrivate *priv;
+
+ g_return_val_if_fail (GSPELL_IS_REGION (region), FALSE);
+
+ priv = _gspell_region_get_instance_private (region);
+
+ if (priv->buffer == NULL ||
+ _gspell_region_is_empty (region))
+ {
+ return FALSE;
+ }
+
+ g_assert (priv->subregions != NULL);
+
+ if (start != NULL)
+ {
+ Subregion *first_subregion = priv->subregions->data;
+ gtk_text_buffer_get_iter_at_mark (priv->buffer, start, first_subregion->start);
+ }
+
+ if (end != NULL)
+ {
+ Subregion *last_subregion = g_list_last (priv->subregions)->data;
+ gtk_text_buffer_get_iter_at_mark (priv->buffer, end, last_subregion->end);
+ }
+
+ return TRUE;
+}
+
+/*
+ * _gspell_region_intersect_subregion:
+ * @region: a #GspellRegion.
+ * @_start: the start of the subregion.
+ * @_end: the end of the subregion.
+ *
+ * Returns the intersection between @region and the subregion delimited by
+ * @_start and @_end. @region is not modified.
+ *
+ * Returns: (transfer full) (nullable): the intersection as a new
+ * #GspellRegion.
+ * Since: 3.22
+ */
+GspellRegion *
+_gspell_region_intersect_subregion (GspellRegion *region,
+ const GtkTextIter *_start,
+ const GtkTextIter *_end)
+{
+ GspellRegionPrivate *priv;
+ GspellRegion *new_region;
+ GspellRegionPrivate *new_priv;
+ GList *start_node;
+ GList *end_node;
+ GList *node;
+ GtkTextIter sr_start_iter;
+ GtkTextIter sr_end_iter;
+ Subregion *sr;
+ Subregion *new_sr;
+ gboolean done;
+ GtkTextIter start;
+ GtkTextIter end;
+
+ g_return_val_if_fail (GSPELL_IS_REGION (region), NULL);
+ g_return_val_if_fail (_start != NULL, NULL);
+ g_return_val_if_fail (_end != NULL, NULL);
+
+ priv = _gspell_region_get_instance_private (region);
+
+ if (priv->buffer == NULL)
+ {
+ return NULL;
+ }
+
+ start = *_start;
+ end = *_end;
+
+ gtk_text_iter_order (&start, &end);
+
+ /* Find bounding subregions. */
+ start_node = find_nearest_subregion (region, &start, NULL, FALSE, FALSE);
+ end_node = find_nearest_subregion (region, &end, start_node, TRUE, FALSE);
+
+ /* Easy case first. */
+ if (start_node == NULL || end_node == NULL || end_node == start_node->prev)
+ {
+ return NULL;
+ }
+
+ new_region = _gspell_region_new (priv->buffer);
+ new_priv = _gspell_region_get_instance_private (new_region);
+ done = FALSE;
+
+ sr = start_node->data;
+ gtk_text_buffer_get_iter_at_mark (priv->buffer, &sr_start_iter, sr->start);
+ gtk_text_buffer_get_iter_at_mark (priv->buffer, &sr_end_iter, sr->end);
+
+ /* Starting node. */
+ if (gtk_text_iter_in_range (&start, &sr_start_iter, &sr_end_iter))
+ {
+ new_sr = g_slice_new0 (Subregion);
+ new_priv->subregions = g_list_prepend (new_priv->subregions, new_sr);
+
+ new_sr->start = gtk_text_buffer_create_mark (new_priv->buffer,
+ NULL,
+ &start,
+ TRUE);
+
+ if (start_node == end_node)
+ {
+ /* Things will finish shortly. */
+ done = TRUE;
+ if (gtk_text_iter_in_range (&end, &sr_start_iter, &sr_end_iter))
+ {
+ new_sr->end = gtk_text_buffer_create_mark (new_priv->buffer,
+ NULL,
+ &end,
+ FALSE);
+ }
+ else
+ {
+ new_sr->end = gtk_text_buffer_create_mark (new_priv->buffer,
+ NULL,
+ &sr_end_iter,
+ FALSE);
+ }
+ }
+ else
+ {
+ new_sr->end = gtk_text_buffer_create_mark (new_priv->buffer,
+ NULL,
+ &sr_end_iter,
+ FALSE);
+ }
+
+ node = start_node->next;
+ }
+ else
+ {
+ /* start should be the same as the subregion, so copy it in the
+ * loop.
+ */
+ node = start_node;
+ }
+
+ if (!done)
+ {
+ while (node != end_node)
+ {
+ /* Copy intermediate subregions verbatim. */
+ sr = node->data;
+ gtk_text_buffer_get_iter_at_mark (priv->buffer, &sr_start_iter, sr->start);
+ gtk_text_buffer_get_iter_at_mark (priv->buffer, &sr_end_iter, sr->end);
+
+ new_sr = g_slice_new0 (Subregion);
+ new_priv->subregions = g_list_prepend (new_priv->subregions, new_sr);
+
+ new_sr->start = gtk_text_buffer_create_mark (new_priv->buffer,
+ NULL,
+ &sr_start_iter,
+ TRUE);
+
+ new_sr->end = gtk_text_buffer_create_mark (new_priv->buffer,
+ NULL,
+ &sr_end_iter,
+ FALSE);
+
+ /* Next node. */
+ node = node->next;
+ }
+
+ /* Ending node. */
+ sr = node->data;
+ gtk_text_buffer_get_iter_at_mark (priv->buffer, &sr_start_iter, sr->start);
+ gtk_text_buffer_get_iter_at_mark (priv->buffer, &sr_end_iter, sr->end);
+
+ new_sr = g_slice_new0 (Subregion);
+ new_priv->subregions = g_list_prepend (new_priv->subregions, new_sr);
+
+ new_sr->start = gtk_text_buffer_create_mark (new_priv->buffer,
+ NULL,
+ &sr_start_iter,
+ TRUE);
+
+ if (gtk_text_iter_in_range (&end, &sr_start_iter, &sr_end_iter))
+ {
+ new_sr->end = gtk_text_buffer_create_mark (new_priv->buffer,
+ NULL,
+ &end,
+ FALSE);
+ }
+ else
+ {
+ new_sr->end = gtk_text_buffer_create_mark (new_priv->buffer,
+ NULL,
+ &sr_end_iter,
+ FALSE);
+ }
+ }
+
+ new_priv->subregions = g_list_reverse (new_priv->subregions);
+ return new_region;
+}
+
+/*
+ * _gspell_region_intersect_region:
+ * @region1: (nullable): a #GspellRegion, or %NULL.
+ * @region2: (nullable): a #GspellRegion, or %NULL.
+ *
+ * Returns the intersection between @region1 and @region2. @region1 and
+ * @region2 are not modified.
+ *
+ * Returns: (transfer full) (nullable): the intersection as a #GspellRegion
+ * object.
+ * Since: 3.22
+ */
+GspellRegion *
+_gspell_region_intersect_region (GspellRegion *region1,
+ GspellRegion *region2)
+{
+ GtkTextBuffer *region1_buffer;
+ GtkTextBuffer *region2_buffer;
+ GspellRegion *full_intersect = NULL;
+ GspellRegionIter region2_iter;
+
+ g_return_val_if_fail (region1 == NULL || GSPELL_IS_REGION (region1), NULL);
+ g_return_val_if_fail (region2 == NULL || GSPELL_IS_REGION (region2), NULL);
+
+ if (region1 == NULL && region2 == NULL)
+ {
+ return NULL;
+ }
+ if (region1 == NULL)
+ {
+ return g_object_ref (region2);
+ }
+ if (region2 == NULL)
+ {
+ return g_object_ref (region1);
+ }
+
+ region1_buffer = _gspell_region_get_buffer (region1);
+ region2_buffer = _gspell_region_get_buffer (region2);
+ g_return_val_if_fail (region1_buffer == region2_buffer, NULL);
+
+ if (region1_buffer == NULL)
+ {
+ return NULL;
+ }
+
+ _gspell_region_get_start_region_iter (region2, ®ion2_iter);
+
+ while (!_gspell_region_iter_is_end (®ion2_iter))
+ {
+ GtkTextIter subregion2_start;
+ GtkTextIter subregion2_end;
+ GspellRegion *sub_intersect;
+
+ if (!_gspell_region_iter_get_subregion (®ion2_iter,
+ &subregion2_start,
+ &subregion2_end))
+ {
+ break;
+ }
+
+ sub_intersect = _gspell_region_intersect_subregion (region1,
+ &subregion2_start,
+ &subregion2_end);
+
+ if (full_intersect == NULL)
+ {
+ full_intersect = sub_intersect;
+ }
+ else
+ {
+ _gspell_region_add_region (full_intersect, sub_intersect);
+ g_clear_object (&sub_intersect);
+ }
+
+ _gspell_region_iter_next (®ion2_iter);
+ }
+
+ return full_intersect;
+}
+
+static gboolean
+check_iterator (GspellRegionIterReal *real)
+{
+ GspellRegionPrivate *priv;
+
+ if (real->region == NULL)
+ {
+ goto invalid;
+ }
+
+ priv = _gspell_region_get_instance_private (real->region);
+
+ if (real->region_timestamp == priv->timestamp)
+ {
+ return TRUE;
+ }
+
+invalid:
+ g_warning ("Invalid GspellRegionIter: either the iterator is "
+ "uninitialized, or the region has been modified since the "
+ "iterator was created.");
+
+ return FALSE;
+}
+
+/*
+ * _gspell_region_get_start_region_iter:
+ * @region: a #GspellRegion.
+ * @iter: (out): iterator to initialize to the first subregion.
+ *
+ * Initializes a #GspellRegionIter to the first subregion of @region. If
+ * @region is empty, @iter will be initialized to the end iterator.
+ *
+ * Since: 3.22
+ */
+void
+_gspell_region_get_start_region_iter (GspellRegion *region,
+ GspellRegionIter *iter)
+{
+ GspellRegionPrivate *priv;
+ GspellRegionIterReal *real;
+
+ g_return_if_fail (GSPELL_IS_REGION (region));
+ g_return_if_fail (iter != NULL);
+
+ priv = _gspell_region_get_instance_private (region);
+ real = (GspellRegionIterReal *)iter;
+
+ /* priv->subregions may be NULL, -> end iter */
+
+ real->region = region;
+ real->subregions = priv->subregions;
+ real->region_timestamp = priv->timestamp;
+}
+
+/*
+ * _gspell_region_iter_is_end:
+ * @iter: a #GspellRegionIter.
+ *
+ * Returns: whether @iter is the end iterator.
+ * Since: 3.22
+ */
+gboolean
+_gspell_region_iter_is_end (GspellRegionIter *iter)
+{
+ GspellRegionIterReal *real;
+
+ g_return_val_if_fail (iter != NULL, FALSE);
+
+ real = (GspellRegionIterReal *)iter;
+ g_return_val_if_fail (check_iterator (real), FALSE);
+
+ return real->subregions == NULL;
+}
+
+/*
+ * _gspell_region_iter_next:
+ * @iter: a #GspellRegionIter.
+ *
+ * Moves @iter to the next subregion.
+ *
+ * Returns: %TRUE if @iter moved and is dereferenceable, or %FALSE if @iter has
+ * been set to the end iterator.
+ * Since: 3.22
+ */
+gboolean
+_gspell_region_iter_next (GspellRegionIter *iter)
+{
+ GspellRegionIterReal *real;
+
+ g_return_val_if_fail (iter != NULL, FALSE);
+
+ real = (GspellRegionIterReal *)iter;
+ g_return_val_if_fail (check_iterator (real), FALSE);
+
+ if (real->subregions != NULL)
+ {
+ real->subregions = real->subregions->next;
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+/*
+ * _gspell_region_iter_get_subregion:
+ * @iter: a #GspellRegionIter.
+ * @start: (out) (optional): iterator to initialize with the subregion start, or %NULL.
+ * @end: (out) (optional): iterator to initialize with the subregion end, or %NULL.
+ *
+ * Gets the subregion at this iterator.
+ *
+ * Returns: %TRUE if @start and @end have been set successfully (if non-%NULL),
+ * or %FALSE if @iter is the end iterator or if the region is empty.
+ * Since: 3.22
+ */
+gboolean
+_gspell_region_iter_get_subregion (GspellRegionIter *iter,
+ GtkTextIter *start,
+ GtkTextIter *end)
+{
+ GspellRegionIterReal *real;
+ GspellRegionPrivate *priv;
+ Subregion *sr;
+
+ g_return_val_if_fail (iter != NULL, FALSE);
+
+ real = (GspellRegionIterReal *)iter;
+ g_return_val_if_fail (check_iterator (real), FALSE);
+
+ if (real->subregions == NULL)
+ {
+ return FALSE;
+ }
+
+ priv = _gspell_region_get_instance_private (real->region);
+
+ if (priv->buffer == NULL)
+ {
+ return FALSE;
+ }
+
+ sr = real->subregions->data;
+ g_return_val_if_fail (sr != NULL, FALSE);
+
+ if (start != NULL)
+ {
+ gtk_text_buffer_get_iter_at_mark (priv->buffer, start, sr->start);
+ }
+
+ if (end != NULL)
+ {
+ gtk_text_buffer_get_iter_at_mark (priv->buffer, end, sr->end);
+ }
+
+ return TRUE;
+}
+
+/*
+ * _gspell_region_to_string:
+ * @region: a #GspellRegion.
+ *
+ * Gets a string represention of @region, for debugging purposes.
+ *
+ * The returned string contains the character offsets of the subregions. It
+ * doesn't include a newline character at the end of the string.
+ *
+ * Returns: (transfer full) (nullable): a string represention of @region. Free
+ * with g_free() when no longer needed.
+ * Since: 3.22
+ */
+gchar *
+_gspell_region_to_string (GspellRegion *region)
+{
+ GspellRegionPrivate *priv;
+ GString *string;
+ GList *l;
+
+ g_return_val_if_fail (GSPELL_IS_REGION (region), NULL);
+
+ priv = _gspell_region_get_instance_private (region);
+
+ if (priv->buffer == NULL)
+ {
+ return NULL;
+ }
+
+ string = g_string_new ("Subregions:");
+
+ for (l = priv->subregions; l != NULL; l = l->next)
+ {
+ Subregion *sr = l->data;
+ GtkTextIter start;
+ GtkTextIter end;
+
+ gtk_text_buffer_get_iter_at_mark (priv->buffer, &start, sr->start);
+ gtk_text_buffer_get_iter_at_mark (priv->buffer, &end, sr->end);
+
+ g_string_append_printf (string,
+ " %d-%d",
+ gtk_text_iter_get_offset (&start),
+ gtk_text_iter_get_offset (&end));
+ }
+
+ return g_string_free (string, FALSE);
+}
diff --git a/gspell/gspellregion.h b/gspell/gspellregion.h
new file mode 100644
index 0000000..80d4b04
--- /dev/null
+++ b/gspell/gspellregion.h
@@ -0,0 +1,125 @@
+/* Do not edit: this file is generated from
https://git.gnome.org/browse/gtksourceview/plain/gtksourceview/gtksourceregion.h */
+
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * gspellregion.h - GtkTextMark-based region utility
+ * This file is part of GtkSourceView
+ *
+ * Copyright (C) 2002 Gustavo Giráldez <gustavo giraldez gmx net>
+ * Copyright (C) 2016 Sébastien Wilmet <swilmet gnome org>
+ *
+ * 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.1 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 Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef GSPELL_REGION_H
+#define GSPELL_REGION_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GSPELL_TYPE_REGION (_gspell_region_get_type ())
+
+G_GNUC_INTERNAL
+G_DECLARE_DERIVABLE_TYPE (GspellRegion, _gspell_region,
+ GSPELL, REGION,
+ GObject)
+
+struct _GspellRegionClass
+{
+ GObjectClass parent_class;
+
+ /* Padding for future expansion */
+ gpointer padding[8];
+};
+
+/*
+ * GspellRegionIter:
+ *
+ * #GspellRegionIter is an opaque datatype; ignore all its fields.
+ * Initialize the iter with _gspell_region_get_start_region_iter().
+ *
+ * Since: 3.22
+ */
+typedef struct _GspellRegionIter GspellRegionIter;
+struct _GspellRegionIter
+{
+ /*< private >*/
+ gpointer dummy1;
+ guint32 dummy2;
+ gpointer dummy3;
+};
+
+G_GNUC_INTERNAL
+GspellRegion * _gspell_region_new (GtkTextBuffer *buffer);
+
+G_GNUC_INTERNAL
+GtkTextBuffer * _gspell_region_get_buffer (GspellRegion *region);
+
+G_GNUC_INTERNAL
+void _gspell_region_add_subregion (GspellRegion *region,
+ const GtkTextIter *_start,
+ const GtkTextIter *_end);
+
+G_GNUC_INTERNAL
+void _gspell_region_add_region (GspellRegion *region,
+ GspellRegion *region_to_add);
+
+G_GNUC_INTERNAL
+void _gspell_region_subtract_subregion (GspellRegion *region,
+ const GtkTextIter *_start,
+ const GtkTextIter *_end);
+
+G_GNUC_INTERNAL
+void _gspell_region_subtract_region (GspellRegion *region,
+ GspellRegion *region_to_subtract);
+
+G_GNUC_INTERNAL
+GspellRegion * _gspell_region_intersect_subregion (GspellRegion *region,
+ const GtkTextIter *_start,
+ const GtkTextIter *_end);
+
+G_GNUC_INTERNAL
+GspellRegion * _gspell_region_intersect_region (GspellRegion *region1,
+ GspellRegion *region2);
+
+G_GNUC_INTERNAL
+gboolean _gspell_region_is_empty (GspellRegion *region);
+
+G_GNUC_INTERNAL
+gboolean _gspell_region_get_bounds (GspellRegion *region,
+ GtkTextIter *start,
+ GtkTextIter *end);
+
+G_GNUC_INTERNAL
+void _gspell_region_get_start_region_iter (GspellRegion *region,
+ GspellRegionIter *iter);
+
+G_GNUC_INTERNAL
+gboolean _gspell_region_iter_is_end (GspellRegionIter *iter);
+
+G_GNUC_INTERNAL
+gboolean _gspell_region_iter_next (GspellRegionIter *iter);
+
+G_GNUC_INTERNAL
+gboolean _gspell_region_iter_get_subregion (GspellRegionIter *iter,
+ GtkTextIter *start,
+ GtkTextIter *end);
+
+G_GNUC_INTERNAL
+gchar * _gspell_region_to_string (GspellRegion *region);
+
+G_END_DECLS
+
+#endif /* GSPELL_REGION_H */
diff --git a/gspell/language-dialog.ui b/gspell/language-dialog.ui
new file mode 100644
index 0000000..e29df72
--- /dev/null
+++ b/gspell/language-dialog.ui
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.19.0 -->
+<interface domain="gspell-1">
+ <requires lib="gtk+" version="3.16"/>
+ <template class="GspellLanguageChooserDialog" parent="GtkDialog">
+ <property name="can_focus">False</property>
+ <property name="title" translatable="yes">Set Language</property>
+ <property name="type_hint">dialog</property>
+ <child internal-child="vbox">
+ <object class="GtkBox" id="dialog-vbox1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">12</property>
+ <property name="margin">12</property>
+ <child>
+ <object class="GtkLabel" id="label1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Select the spell checking _language.</property>
+ <property name="use_underline">True</property>
+ <property name="wrap">True</property>
+ <property name="mnemonic_widget">treeview</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolledwindow1">
+ <property name="height_request">180</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="shadow_type">etched-in</property>
+ <child>
+ <object class="GtkTreeView" id="treeview">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="headers_visible">False</property>
+ <child internal-child="selection">
+ <object class="GtkTreeSelection" id="treeview-selection1"/>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child type="action">
+ <object class="GtkButton" id="button_cancel">
+ <property name="visible">True</property>
+ <property name="use_underline">True</property>
+ <property name="label" translatable="yes">_Cancel</property>
+ </object>
+ </child>
+ <child type="action">
+ <object class="GtkButton" id="button_ok">
+ <property name="visible">True</property>
+ <property name="use_underline">True</property>
+ <property name="label" translatable="yes">_Select</property>
+ <property name="can-default">True</property>
+ </object>
+ </child>
+ <action-widgets>
+ <action-widget response="cancel">button_cancel</action-widget>
+ <action-widget response="ok" default="true">button_ok</action-widget>
+ </action-widgets>
+ </template>
+</interface>
diff --git a/gspell/meson.build b/gspell/meson.build
new file mode 100644
index 0000000..4fdd672
--- /dev/null
+++ b/gspell/meson.build
@@ -0,0 +1,51 @@
+libgspell_sources = [
+ 'gspell-checker.c',
+ 'gspell-checker-dialog.c',
+ 'gspell-context-menu.c',
+ 'gspell-current-word-policy.c',
+ 'gspell-entry-buffer.c',
+ 'gspell-entry.c',
+ 'gspell-entry-utils.c',
+ 'gspell-icu.c',
+ 'gspell-init.c',
+ 'gspell-inline-checker-text-buffer.c',
+ 'gspell-language.c',
+ 'gspell-language-chooser-button.c',
+ 'gspell-language-chooser.c',
+ 'gspell-language-chooser-dialog.c',
+ 'gspell-navigator.c',
+ 'gspell-navigator-text-view.c',
+ 'gspellregion.c',
+ 'gspell-text-buffer.c',
+ 'gspell-text-iter.c',
+ 'gspell-text-view.c',
+ 'gspell-utils.c',
+]
+
+libgspell_sources += gnome.compile_resources('gspell-resource',
+ 'gspell.gresource.xml',
+ c_name: 'gspell'
+)
+
+if host_machine.system() == 'darwin'
+ libgspell_sources += ['gspell-osx.c']
+endif
+
+libgspell_deps = [
+ libgtk_dep,
+ libenchant_dep,
+]
+
+libgspell = static_library('libgspell', libgspell_sources,
+ dependencies: libgspell_deps,
+ include_directories: [include_directories('.'), include_directories('..')],
+ c_args: ['-DGSPELL_COMPILATION',
+ '-DDATADIR="{0}"'.format(join_paths('datadir')),
+ '-DGETTEXT_PACKAGE="gnome-text-editor"' ],
+)
+
+libgspell_dep = declare_dependency(
+ dependencies: libgspell_deps,
+ include_directories: include_directories('.'),
+ link_whole: libgspell,
+)
diff --git a/meson.build b/meson.build
index 153525d..efa7521 100644
--- a/meson.build
+++ b/meson.build
@@ -21,15 +21,18 @@ endif
glib_req_version = '2.69'
gtk_req_version = '4.3'
gtksourceview_req_version = '5.0'
+enchant_req_version = '2.2.12'
glib_req = '>= @0@'.format(glib_req_version)
gtk_req = '>= @0@'.format(gtk_req_version)
gtksourceview_req = '>= @0@'.format(gtksourceview_req_version)
+enchant_req = '>= @0@'.format(enchant_req_version)
libglib_dep = dependency('gio-unix-2.0', version: glib_req)
libgtk_dep = dependency('gtk4', version: gtk_req)
libgtksourceview_dep = dependency('gtksourceview-5', version: gtksourceview_req)
libadwaita_dep = dependency('libadwaita-1')
+libenchant_dep = dependency('enchant-2', version: enchant_req)
# Specify minimum library versions
glib_major = glib_req_version.split('.')[0].to_int()
@@ -154,6 +157,7 @@ foreach link_arg: test_link_args
endforeach
subdir('data')
+subdir('gspell')
subdir('src')
subdir('po')
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]