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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user