[gnome-text-editor/wip/chergert/gspell] wip: stub out cribbing of gspell




commit a8eaaea08f40a7ff502b1f4c029abaf591d71ba3
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,
+                                                &current_word_start,
+                                                &current_word_end))
+               {
+                       remove_highlight_tag_if_present (spell,
+                                                        &current_word_start,
+                                                        &current_word_end);
+
+                       _gspell_region_subtract_subregion (intersect,
+                                                          &current_word_start,
+                                                          &current_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,
+                                                     &current_word_start,
+                                                     &current_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, &region_iter);
+ *
+ * while (!_gspell_region_iter_is_end (&region_iter))
+ * {
+ *         GtkTextIter subregion_start;
+ *         GtkTextIter subregion_end;
+ *
+ *         if (!_gspell_region_iter_get_subregion (&region_iter,
+ *                                                    &subregion_start,
+ *                                                    &subregion_end))
+ *         {
+ *                 break;
+ *         }
+ *
+ *         // Do something useful with the subregion.
+ *
+ *         _gspell_region_iter_next (&region_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, &region_iter);
+
+       while (!_gspell_region_iter_is_end (&region_iter))
+       {
+               GtkTextIter subregion_start;
+               GtkTextIter subregion_end;
+
+               if (!_gspell_region_iter_get_subregion (&region_iter,
+                                                          &subregion_start,
+                                                          &subregion_end))
+               {
+                       return TRUE;
+               }
+
+               if (!gtk_text_iter_equal (&subregion_start, &subregion_end))
+               {
+                       return FALSE;
+               }
+
+               _gspell_region_iter_next (&region_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, &region2_iter);
+
+       while (!_gspell_region_iter_is_end (&region2_iter))
+       {
+               GtkTextIter subregion2_start;
+               GtkTextIter subregion2_end;
+               GspellRegion *sub_intersect;
+
+               if (!_gspell_region_iter_get_subregion (&region2_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 (&region2_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]