textbuffer: add rudimentary spell checking

This only checks spelling and does not yet hook up into anything such as
suggestions. It must be driven by the display to force checking a visible
region. To enable, set the :spell-checker property.

Longer term we'll want something more advanced than the simple iteration
through words that is done here. We need to have regions that are not to
be spell checked so that GtkSourceView can continue to work as expected.

We also need to apply the correct language from runs of text which would
benefit from being connected to a language (and PangoLanguage may not be
suitable there).
This commit is contained in:
Christian Hergert
2021-03-29 21:35:23 -07:00
parent 5e3f1ee42a
commit a24281e900
3 changed files with 234 additions and 12 deletions

View File

@@ -33,12 +33,17 @@
#include "gtktextbufferprivate.h"
#include "gtktextbtree.h"
#include "gtktextiterprivate.h"
#include "gtktextregionprivate.h"
#include "gtkspellcheckprivate.h"
#include "gtktexttagprivate.h"
#include "gtktexttagtableprivate.h"
#include "gtkprivate.h"
#include "gtkintl.h"
#define DEFAULT_MAX_UNDO 200
#define DEFAULT_MAX_UNDO 200
#define SPELLING_UNCHECKED GSIZE_TO_POINTER(0)
#define SPELLING_CHECKED GSIZE_TO_POINTER(1)
/**
* GtkTextBuffer:
@@ -65,6 +70,10 @@ struct _GtkTextBufferPrivate
GtkTextHistory *history;
GtkTextRegion *spell_region;
GtkSpellChecker *spell_checker;
GtkTextTag *spell_tag;
guint user_action_count;
/* Whether the buffer has been modified since last save */
@@ -116,10 +125,12 @@ enum {
PROP_CAN_UNDO,
PROP_CAN_REDO,
PROP_ENABLE_UNDO,
PROP_SPELL_CHECKER,
LAST_PROP
};
static void gtk_text_buffer_finalize (GObject *object);
static void gtk_text_buffer_constructed (GObject *object);
static void gtk_text_buffer_finalize (GObject *object);
static void gtk_text_buffer_real_insert_text (GtkTextBuffer *buffer,
GtkTextIter *iter,
@@ -431,6 +442,7 @@ gtk_text_buffer_class_init (GtkTextBufferClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
object_class->constructed = gtk_text_buffer_constructed;
object_class->finalize = gtk_text_buffer_finalize;
object_class->set_property = gtk_text_buffer_set_property;
object_class->get_property = gtk_text_buffer_get_property;
@@ -535,6 +547,22 @@ gtk_text_buffer_class_init (GtkTextBufferClass *klass)
0,
GTK_PARAM_READABLE);
/**
* GtkTextBuffer:spell-checker:
*
* The #GtkSpellChecker to use for spell checking the buffer.
*
* If set, the buffer will be scanned for misspelled words.
*
* Since: 4.2
*/
text_buffer_props[PROP_SPELL_CHECKER] =
g_param_spec_object ("spell-checker",
"Spell Checker",
"The spell checker for the buffer",
GTK_TYPE_SPELL_CHECKER,
(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
g_object_class_install_properties (object_class, LAST_PROP, text_buffer_props);
/**
@@ -980,6 +1008,10 @@ gtk_text_buffer_set_property (GObject *object,
gtk_text_buffer_set_enable_undo (text_buffer, g_value_get_boolean (value));
break;
case PROP_SPELL_CHECKER:
gtk_text_buffer_set_spell_checker (text_buffer, g_value_get_object (value));
break;
case PROP_TAG_TABLE:
set_table (text_buffer, g_value_get_object (value));
break;
@@ -1012,6 +1044,10 @@ gtk_text_buffer_get_property (GObject *object,
g_value_set_boolean (value, gtk_text_buffer_get_enable_undo (text_buffer));
break;
case PROP_SPELL_CHECKER:
g_value_set_object (value, gtk_text_buffer_get_spell_checker (text_buffer));
break;
case PROP_TAG_TABLE:
g_value_set_object (value, get_table (text_buffer));
break;
@@ -1071,6 +1107,19 @@ gtk_text_buffer_new (GtkTextTagTable *table)
return text_buffer;
}
static void
gtk_text_buffer_constructed (GObject *object)
{
GtkTextBuffer *buffer = GTK_TEXT_BUFFER (object);
G_OBJECT_CLASS (gtk_text_buffer_parent_class)->constructed (object);
buffer->priv->spell_tag =
gtk_text_buffer_create_tag (buffer, NULL,
"underline", PANGO_UNDERLINE_ERROR,
NULL);
}
static void
gtk_text_buffer_finalize (GObject *object)
{
@@ -1082,6 +1131,9 @@ gtk_text_buffer_finalize (GObject *object)
remove_all_selection_clipboards (buffer);
g_clear_pointer (&buffer->priv->spell_region, _gtk_text_region_free);
g_clear_object (&buffer->priv->spell_checker);
g_clear_object (&buffer->priv->history);
if (priv->tag_table)
@@ -1196,6 +1248,12 @@ gtk_text_buffer_real_insert_text (GtkTextBuffer *buffer,
text,
len);
if (buffer->priv->spell_region != NULL)
_gtk_text_region_insert (buffer->priv->spell_region,
gtk_text_iter_get_offset (iter),
g_utf8_strlen (text, len),
SPELLING_UNCHECKED);
_gtk_text_btree_insert (iter, text, len);
g_signal_emit (buffer, signals[CHANGED], 0);
@@ -1953,6 +2011,11 @@ gtk_text_buffer_real_delete_range (GtkTextBuffer *buffer,
g_free (text);
}
if (buffer->priv->spell_region != NULL)
_gtk_text_region_remove (buffer->priv->spell_region,
gtk_text_iter_get_offset (start),
gtk_text_iter_get_offset (end) - gtk_text_iter_get_offset (start));
_gtk_text_btree_delete (start, end);
/* may have deleted the selection... */
@@ -5082,3 +5145,154 @@ gtk_text_buffer_set_max_undo_levels (GtkTextBuffer *buffer,
gtk_text_history_set_max_undo_levels (buffer->priv->history, max_undo_levels);
}
/**
* gtk_text_buffer_get_spell_checker:
* @buffer: a #GtkTextBuffer
*
* Get the #GtkTextBuffer:spell-checker property.
*
* Returns: (transfer none): a #GtkSpellChecker or %NULL
*
* Since: 4.2
*/
GtkSpellChecker *
gtk_text_buffer_get_spell_checker (GtkTextBuffer *buffer)
{
g_return_val_if_fail (GTK_IS_TEXT_BUFFER (buffer), NULL);
return buffer->priv->spell_checker;
}
/**
* gtk_text_buffer_set_spell_checker:
* @buffer: a #GtkTextBuffer
* @spell_checker: (nullable): a #GtkSpellChecker
*
* Sets the #GtkTextBuffer:spell-checker property.
* Set this to enable spell checking on your #GtkTextBuffer.
*
* Since: 4.2
*/
void
gtk_text_buffer_set_spell_checker (GtkTextBuffer *buffer,
GtkSpellChecker *spell_checker)
{
g_return_if_fail (GTK_IS_TEXT_BUFFER (buffer));
g_return_if_fail (!spell_checker || GTK_IS_SPELL_CHECKER (spell_checker));
if (g_set_object (&buffer->priv->spell_checker, spell_checker))
{
g_clear_pointer (&buffer->priv->spell_region, _gtk_text_region_free);
if (spell_checker != NULL)
{
GtkTextIter end;
buffer->priv->spell_region = _gtk_text_region_new (NULL, NULL);
gtk_text_buffer_get_end_iter (buffer, &end);
_gtk_text_region_insert (buffer->priv->spell_region,
0, gtk_text_iter_get_offset (&end),
SPELLING_UNCHECKED);
}
g_object_notify_by_pspec (G_OBJECT (buffer), text_buffer_props [PROP_SPELL_CHECKER]);
}
}
gboolean
_gtk_text_buffer_can_check_spelling (GtkTextBuffer *buffer)
{
g_return_val_if_fail (GTK_IS_TEXT_BUFFER (buffer), FALSE);
return buffer->priv->spell_checker != NULL;
}
static gboolean
has_unchecked_ranges_cb (gsize offset,
const GtkTextRegionRun *run,
gpointer user_data)
{
gboolean *has_unchecked = user_data;
g_assert (run != NULL);
g_assert (has_unchecked != NULL);
if (run->data == SPELLING_UNCHECKED)
{
*has_unchecked = TRUE;
return TRUE;
}
return FALSE;
}
void
_gtk_text_buffer_check_spelling (GtkTextBuffer *buffer,
const GtkTextIter *begin,
const GtkTextIter *end)
{
GtkTextIter iter;
guint has_unchecked = 0;
guint begin_offset;
guint end_offset;
g_return_if_fail (GTK_IS_TEXT_BUFFER (buffer));
g_return_if_fail (begin != NULL);
g_return_if_fail (end != NULL);
if (buffer->priv->spell_checker == NULL)
return;
g_assert (buffer->priv->spell_region != NULL);
begin_offset = gtk_text_iter_get_offset (begin);
end_offset = gtk_text_iter_get_offset (end);
if (begin_offset == end_offset)
return;
g_assert (begin_offset < end_offset);
_gtk_text_region_foreach_in_range (buffer->priv->spell_region,
begin_offset, end_offset,
has_unchecked_ranges_cb,
&has_unchecked);
if (!has_unchecked)
return;
iter = *begin;
if (!gtk_text_iter_starts_word (&iter))
gtk_text_iter_backward_word_start (&iter);
while (gtk_text_iter_compare (&iter, end) < 0)
{
GtkTextIter word_end = iter;
char *word;
if (!gtk_text_iter_forward_word_end (&word_end))
break;
word = gtk_text_iter_get_slice (&iter, &word_end);
if (!_gtk_spell_checker_contains_word (buffer->priv->spell_checker, word, -1))
gtk_text_buffer_apply_tag (buffer, buffer->priv->spell_tag, &iter, &word_end);
if (!gtk_text_iter_forward_word_end (&word_end))
break;
iter = word_end;
if (!gtk_text_iter_backward_word_start (&iter))
break;
g_free (word);
}
_gtk_text_region_replace (buffer->priv->spell_region,
begin_offset,
end_offset - begin_offset,
SPELLING_CHECKED);
}

View File

@@ -34,6 +34,7 @@
#include <gtk/gtktextiter.h>
#include <gtk/gtktextmark.h>
#include <gtk/gtktextchild.h>
#include <gtk/gtkspellcheck.h>
G_BEGIN_DECLS
@@ -461,6 +462,12 @@ void gtk_text_buffer_begin_user_action (GtkTextBuffer *buffer
GDK_AVAILABLE_IN_ALL
void gtk_text_buffer_end_user_action (GtkTextBuffer *buffer);
GDK_AVAILABLE_IN_4_2
GtkSpellChecker *gtk_text_buffer_get_spell_checker (GtkTextBuffer *buffer);
GDK_AVAILABLE_IN_4_2
void gtk_text_buffer_set_spell_checker (GtkTextBuffer *buffer,
GtkSpellChecker *spell_checker);
G_END_DECLS

View File

@@ -23,16 +23,17 @@
G_BEGIN_DECLS
void _gtk_text_buffer_spew (GtkTextBuffer *buffer);
GtkTextBTree* _gtk_text_buffer_get_btree (GtkTextBuffer *buffer);
const PangoLogAttr* _gtk_text_buffer_get_line_log_attrs (GtkTextBuffer *buffer,
const GtkTextIter *anywhere_in_line,
int *char_len);
void _gtk_text_buffer_notify_will_remove_tag (GtkTextBuffer *buffer,
GtkTextTag *tag);
void _gtk_text_buffer_spew (GtkTextBuffer *buffer);
GtkTextBTree *_gtk_text_buffer_get_btree (GtkTextBuffer *buffer);
const PangoLogAttr *_gtk_text_buffer_get_line_log_attrs (GtkTextBuffer *buffer,
const GtkTextIter *anywhere_in_line,
int *char_len);
void _gtk_text_buffer_notify_will_remove_tag (GtkTextBuffer *buffer,
GtkTextTag *tag);
gboolean _gtk_text_buffer_can_check_spelling (GtkTextBuffer *buffer);
void _gtk_text_buffer_check_spelling (GtkTextBuffer *buffer,
const GtkTextIter *begin,
const GtkTextIter *end);
G_END_DECLS