Compare commits

...

16 Commits

Author SHA1 Message Date
Christian Hergert
dd7ce2e1ac build: relax enchant-2 requirement 2021-03-30 13:18:27 -07:00
Christian Hergert
1715f77868 spellcheck: handle failure to create a spell check object 2021-03-30 13:13:54 -07:00
Christian Hergert
c68eb0a064 temp: set default spell checker (remove me) 2021-03-30 13:07:30 -07:00
Christian Hergert
a35cd721bb textview: check spelling for visible region 2021-03-30 13:07:30 -07:00
Christian Hergert
a24281e900 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).
2021-03-30 13:07:30 -07:00
Christian Hergert
5e3f1ee42a textregion: allow breaking foreach early 2021-03-30 13:07:30 -07:00
Christian Hergert
1264245601 gtk: add GtkTextRegion
GtkTextRegion is a helper for tracking regions of text when you might not
be able to use GtkTextBuffer's B-Tree. This can be useful when writing
code that needs to work with GtkEditable and GtkTextView both.

This is implemented in a fashion that resembles the combination of a
B+Tree (doubly-linked internal and leaf nodes) and a piecetable.

The goal for this is to be able to track regions of a buffer that need
updating as we process GtkTextView drawing.

A number of tests are provided to ensure correctness of the data structure.
2021-03-30 13:07:30 -07:00
Christian Hergert
3f6b2d9b9f spellcheck: deduplicate languages from checker
We don't want the same language loaded twice which could occur if we had
something like "en_US" and "en_US.UTF-8" which is turned into "en_US" by
enchant internally.
2021-03-30 13:07:30 -07:00
Christian Hergert
cbd29b4ce9 spellcheck: replace language code with loaded language
If enchant returns us a dictionary that is slightly different than the one
we requested, then replace the language code with what was loaded. This
can be used to deduplicate later.
2021-03-30 13:07:30 -07:00
Christian Hergert
f25e53bd45 spellcheck: check for valid language 2021-03-30 13:07:30 -07:00
Christian Hergert
cb86fcc6d5 spellcheck: allow language init to fail 2021-03-30 13:07:30 -07:00
Christian Hergert
c77de7d328 spellcheck: add series of fallbacks when loading default 2021-03-30 13:07:30 -07:00
Christian Hergert
71a317b01e spellcheck: return NULL if no langugaes were loaded 2021-03-30 13:07:30 -07:00
Christian Hergert
78f970175d spellcheck: reuse helper for languages list 2021-03-30 13:07:30 -07:00
Christian Hergert
f390043e90 spellcheck: handle ownership transfer 2021-03-30 13:07:30 -07:00
Christian Hergert
15abded486 spellcheck: stub out spellcheck API 2021-03-30 13:07:29 -07:00
21 changed files with 3668 additions and 13 deletions

View File

@@ -30,6 +30,9 @@
/* Define to 1 if you have the <dlfcn.h> header file. */
#mesondefine HAVE_DLFCN_H
#
/* define if we have enchant */
#mesondefine HAVE_ENCHANT
/* Have the ffmpeg library */
#mesondefine HAVE_FFMPEG

View File

@@ -587,7 +587,7 @@ gdk_keymap_lookup_key (GdkKeymap *keymap,
* (state & ~consumed & ALL_ACCELS_MASK) == GDK_CONTROL_MASK)
* // Control was pressed
* ]|
*
*
* An older interpretation @consumed_modifiers was that it contained
* all modifiers that might affect the translation of the key;
* this allowed accelerators to be stored with irrelevant consumed

View File

@@ -53,6 +53,8 @@ struct _GdkWaylandKeymap
PangoDirection *direction;
gboolean bidi;
char **languages;
};
struct _GdkWaylandKeymapClass

View File

@@ -236,6 +236,7 @@
#include <gtk/gtkstacksidebar.h>
#include <gtk/gtksizegroup.h>
#include <gtk/gtksizerequest.h>
#include <gtk/gtkspellcheck.h>
#include <gtk/gtkspinbutton.h>
#include <gtk/gtkspinner.h>
#include <gtk/gtkstack.h>

543
gtk/gtkspellcheck.c Normal file
View File

@@ -0,0 +1,543 @@
/*
* Copyright 2021 Christian Hergert <chergert@redhat.com>
*
* 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 of the
* licence 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/>.
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "config.h"
#ifdef HAVE_ENCHANT
# include <enchant.h>
#endif
#include "gtkflattenlistmodel.h"
#include "gtkspellcheckprivate.h"
#include "gtkstringlist.h"
#ifdef HAVE_ENCHANT
static char **gtk_enchant_list_languages (void);
static gboolean gtk_enchant_supports (const char *code);
static gboolean gtk_enchant_contains_word (GtkSpellLanguage *language,
const char *word,
gssize word_length);
static gboolean gtk_enchant_init_language (GtkSpellLanguage *language);
static void gtk_enchant_fini_language (GtkSpellLanguage *language);
static GListModel *gtk_enchant_list_corrections (GtkSpellLanguage *language,
const char *word,
gssize word_length);
#endif
G_DEFINE_TYPE (GtkSpellChecker, gtk_spell_checker, G_TYPE_OBJECT)
enum {
PROP_0,
PROP_LANGUAGES,
N_PROPS
};
static GParamSpec *properties [N_PROPS];
static const GtkSpellProvider providers[] = {
#ifdef HAVE_ENCHANT
{
.name = "enchant",
.supports = gtk_enchant_supports,
.contains_word = gtk_enchant_contains_word,
.list_languages = gtk_enchant_list_languages,
.init_language = gtk_enchant_init_language,
.fini_language = gtk_enchant_fini_language,
.list_corrections = gtk_enchant_list_corrections,
},
#endif
};
static gboolean
gtk_spell_provider_supports (const GtkSpellProvider *provider,
const char *code)
{
char **codes;
gboolean ret = FALSE;
if (provider->supports != NULL)
return provider->supports (code);
/* Fallback based on listing languages */
if ((codes = provider->list_languages ()))
{
for (guint i = 0; codes[i]; i++)
{
if (g_strcmp0 (codes[i], code) == 0)
{
ret = TRUE;
break;
}
}
}
g_strfreev (codes);
return ret;
}
static void
_gtk_source_language_free (GtkSpellLanguage *language)
{
if (language->provider->fini_language)
language->provider->fini_language (language);
g_clear_pointer (&language->code, g_free);
language->provider = NULL;
language->native = NULL;
g_free (language);
}
static GtkSpellLanguage *
_gtk_spell_language_new (const GtkSpellProvider *provider,
const char *code)
{
GtkSpellLanguage *language;
g_assert (provider != NULL);
g_assert (code != NULL);
language = g_new0 (GtkSpellLanguage, 1);
language->provider = provider;
language->code = g_strdup (code);
if (provider->init_language != NULL)
{
if (!provider->init_language (language))
{
g_free (language->code);
g_free (language);
return NULL;
}
}
return language;
}
static char **
gtk_spell_checker_get_languages (GtkSpellChecker *self)
{
GArray *ar = g_array_new (TRUE, FALSE, sizeof (char *));
for (guint i = 0; i < self->languages->len; i++)
{
GtkSpellLanguage *language = g_ptr_array_index (self->languages, i);
char *code = g_strdup (language->code);
g_array_append_val (ar, code);
}
return (char **)(gpointer)g_array_free (ar, FALSE);
}
static gboolean
gtk_spell_checker_contains_language (GtkSpellChecker *self,
GtkSpellLanguage *language)
{
g_assert (GTK_IS_SPELL_CHECKER (self));
g_assert (language != NULL);
g_assert (language->code != NULL);
g_assert (language->provider != NULL);
for (guint i = 0; i < self->languages->len; i++)
{
GtkSpellLanguage *element = g_ptr_array_index (self->languages, i);
if (g_strcmp0 (language->code, element->code) == 0)
return TRUE;
}
return FALSE;
}
static void
gtk_spell_checker_set_languages (GtkSpellChecker *self,
const char * const *languages)
{
for (guint i = 0; languages[i]; i++)
{
const char *code = languages[i];
for (guint j = 0; j < G_N_ELEMENTS (providers); j++)
{
const GtkSpellProvider *provider = &providers[j];
if (gtk_spell_provider_supports (provider, code))
{
GtkSpellLanguage *language = _gtk_spell_language_new (provider, code);
if (language == NULL)
continue;
if (!gtk_spell_checker_contains_language (self, language))
g_ptr_array_add (self->languages, language);
break;
}
}
}
}
static void
gtk_spell_checker_finalize (GObject *object)
{
GtkSpellChecker *self = (GtkSpellChecker *)object;
g_clear_pointer (&self->languages, g_ptr_array_unref);
G_OBJECT_CLASS (gtk_spell_checker_parent_class)->finalize (object);
}
static void
gtk_spell_checker_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
GtkSpellChecker *self = GTK_SPELL_CHECKER (object);
switch (prop_id)
{
case PROP_LANGUAGES:
g_value_take_boxed (value, gtk_spell_checker_get_languages (self));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static void
gtk_spell_checker_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
GtkSpellChecker *self = GTK_SPELL_CHECKER (object);
switch (prop_id)
{
case PROP_LANGUAGES:
gtk_spell_checker_set_languages (self, g_value_get_boxed (value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
}
static void
gtk_spell_checker_class_init (GtkSpellCheckerClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
object_class->finalize = gtk_spell_checker_finalize;
object_class->get_property = gtk_spell_checker_get_property;
object_class->set_property = gtk_spell_checker_set_property;
properties [PROP_LANGUAGES] =
g_param_spec_boxed ("languages",
"Languages",
"The language codes to support",
G_TYPE_STRV,
(G_PARAM_READWRITE |
G_PARAM_CONSTRUCT_ONLY |
G_PARAM_STATIC_STRINGS));
g_object_class_install_properties (object_class, N_PROPS, properties);
}
static void
gtk_spell_checker_init (GtkSpellChecker *self)
{
self->languages = g_ptr_array_new_with_free_func ((GDestroyNotify)_gtk_source_language_free);
}
/**
* gtk_spell_checker_new_for_language:
* @language: (nullable): the language to support
*
* Creates a new #GtkSpellChecker which uses a dictionary available based
* on @language.
*
* Returns: (transfer full): a #GtkSpellChecker
*
* Since: 4.2
*/
GtkSpellChecker *
gtk_spell_checker_new_for_language (const char *language)
{
const char *languages[] = { language, NULL };
return gtk_spell_checker_new_for_languages (languages);
}
/**
* gtk_spell_checker_new_for_languages:
* @languages: (nullable) (element-type utf8): the language codes to support
*
* Creates a new #GtkSpellChecker which uses dictionaries available based
* on @languages.
*
* Returns: (transfer full): a #GtkSpellChecker
*
* Since: 4.2
*/
GtkSpellChecker *
gtk_spell_checker_new_for_languages (const char * const * languages)
{
GtkSpellChecker *ret;
ret = g_object_new (GTK_TYPE_SPELL_CHECKER,
"languages", languages,
NULL);
if (ret != NULL && ret->languages->len == 0)
g_clear_object (&ret);
return g_steal_pointer (&ret);
}
const char * const *
gtk_spell_checker_list_languages (void)
{
static char **languages;
if (languages == NULL)
{
GHashTable *seen = g_hash_table_new (g_str_hash, g_str_equal);
GArray *ar = g_array_new (TRUE, FALSE, sizeof (char *));
for (guint i = 0; i < G_N_ELEMENTS (providers); i++)
{
const GtkSpellProvider *provider = &providers[i];
char **found = provider->list_languages ();
if (found == NULL)
continue;
for (guint j = 0; found[j]; j++)
{
if (!g_hash_table_contains (seen, found[j]))
{
char *copy = g_strdup (found[j]);
g_array_append_val (ar, copy);
g_hash_table_add (seen, copy);
}
}
g_strfreev (found);
}
languages = (char **)(gpointer)g_array_free (ar, FALSE);
g_hash_table_unref (seen);
}
return (const char * const *)languages;
}
gboolean
gtk_spell_checker_contains_word (GtkSpellChecker *self,
const char *word,
gssize word_length)
{
g_return_val_if_fail (GTK_IS_SPELL_CHECKER (self), FALSE);
return _gtk_spell_checker_contains_word (self, word, word_length);
}
GListModel *
gtk_spell_checker_list_corrections (GtkSpellChecker *self,
const char *word,
gssize word_length)
{
GtkFlattenListModel *ret;
GListStore *store;
g_return_val_if_fail (GTK_IS_SPELL_CHECKER (self), NULL);
g_return_val_if_fail (word != NULL, NULL);
if (word_length < 0)
word_length = strlen (word);
if (self->languages->len == 0)
return G_LIST_MODEL (gtk_string_list_new (NULL));
if (self->languages->len == 1)
{
GtkSpellLanguage *language = g_ptr_array_index (self->languages, 0);
return language->provider->list_corrections (language, word, word_length);
}
store = g_list_store_new (G_TYPE_LIST_MODEL);
for (guint i = 0; i < self->languages->len; i++)
{
GtkSpellLanguage *language = g_ptr_array_index (self->languages, i);
GListModel *model = language->provider->list_corrections (language, word, word_length);
if (model != NULL)
{
g_list_store_append (store, model);
g_object_unref (model);
}
}
ret = gtk_flatten_list_model_new (G_LIST_MODEL (store));
return G_LIST_MODEL (ret);
}
GtkSpellChecker *
gtk_spell_checker_get_default (void)
{
static GtkSpellChecker *instance;
if (instance == NULL)
{
const char * const *langs = g_get_language_names ();
if (langs != NULL)
instance = gtk_spell_checker_new_for_languages (langs);
if (instance == NULL)
instance = gtk_spell_checker_new_for_language ("en_US");
if (instance == NULL)
instance = gtk_spell_checker_new_for_language ("C");
/* TODO: We might want to have a fallback so that a real object
* is always returned from this method.
*/
if (instance != NULL)
g_object_add_weak_pointer (G_OBJECT (instance), (gpointer *)&instance);
}
return instance;
}
#ifdef HAVE_ENCHANT
static EnchantBroker *
gtk_enchant_get_broker (void)
{
static EnchantBroker *broker;
if (broker == NULL)
broker = enchant_broker_init ();
return broker;
}
static gboolean
gtk_enchant_supports (const char *code)
{
return enchant_broker_dict_exists (gtk_enchant_get_broker (), code);
}
static void
gtk_enchant_list_languages_cb (const char * const lang_tag,
const char * const provider_name,
const char * const provider_desc,
const char * const provider_file,
void *user_data)
{
GArray *ar = user_data;
char *code = g_strdup (lang_tag);
g_array_append_val (ar, code);
}
static char **
gtk_enchant_list_languages (void)
{
EnchantBroker *broker = gtk_enchant_get_broker ();
GArray *ar = g_array_new (TRUE, FALSE, sizeof (char *));
enchant_broker_list_dicts (broker,
gtk_enchant_list_languages_cb,
ar);
return (char **)(gpointer)g_array_free (ar, FALSE);
}
static gboolean
gtk_enchant_contains_word (GtkSpellLanguage *language,
const char *word,
gssize word_length)
{
return enchant_dict_check (language->native, word, word_length) == 0;
}
static GListModel *
gtk_enchant_list_corrections (GtkSpellLanguage *language,
const char *word,
gssize word_length)
{
size_t count = 0;
char **ret = enchant_dict_suggest (language->native, word, word_length, &count);
GtkStringList *model = gtk_string_list_new ((const char * const *)ret);
enchant_dict_free_string_list (language->native, ret);
return G_LIST_MODEL (model);
}
static void
gtk_enchant_init_language_cb (const char * const code,
const char * const provider_name,
const char * const provider_desc,
const char * const provider_file,
void *user_data)
{
GtkSpellLanguage *language = user_data;
g_assert (language != NULL);
g_assert (code != NULL);
/* Replace the language code so we can deduplicate based on what
* dictionary was actually loaded. Otherwise we could end up with
* en_US and en_US.UTF-8 as two separate dictionaries.
*/
g_free (language->code);
language->code = g_strdup (code);
}
static gboolean
gtk_enchant_init_language (GtkSpellLanguage *language)
{
EnchantBroker *broker = gtk_enchant_get_broker ();
if (!(language->native = enchant_broker_request_dict (broker, language->code)))
return FALSE;
enchant_dict_describe (language->native,
gtk_enchant_init_language_cb,
language);
return TRUE;
}
static void
gtk_enchant_fini_language (GtkSpellLanguage *language)
{
if (language->native != NULL)
{
enchant_broker_free_dict (gtk_enchant_get_broker (), language->native);
language->native = NULL;
}
}
#endif

69
gtk/gtkspellcheck.h Normal file
View File

@@ -0,0 +1,69 @@
/*
* Copyright 2021 Christian Hergert <chergert@redhat.com>
*
* 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 of the
* licence 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/>.
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#ifndef __GTK_SPELL_CHECK_H__
#define __GTK_SPELL_CHECK_H__
#include <gdk/gdk.h>
G_BEGIN_DECLS
#define GTK_TYPE_SPELL_CHECKER (gtk_spell_checker_get_type())
typedef enum _GtkSpellDictionary
{
GTK_SPELL_DICTIONARY_SESSION,
GTK_SPELL_DICTIONARY_PERSONAL,
} GtkSpellDictionary;
GDK_AVAILABLE_IN_4_2
G_DECLARE_FINAL_TYPE (GtkSpellChecker, gtk_spell_checker, GTK, SPELL_CHECKER, GObject)
GDK_AVAILABLE_IN_4_2
GtkSpellChecker *gtk_spell_checker_get_default (void);
GDK_AVAILABLE_IN_4_2
GtkSpellChecker *gtk_spell_checker_new_for_language (const char *language);
GDK_AVAILABLE_IN_4_2
GtkSpellChecker *gtk_spell_checker_new_for_languages (const char * const *languages);
GDK_AVAILABLE_IN_4_2
const char * const *gtk_spell_checker_list_languages (void);
GDK_AVAILABLE_IN_4_2
gboolean gtk_spell_checker_contains_word (GtkSpellChecker *self,
const char *word,
gssize word_length);
GDK_AVAILABLE_IN_4_2
GListModel *gtk_spell_checker_list_corrections (GtkSpellChecker *self,
const char *word,
gssize word_length);
GDK_AVAILABLE_IN_4_2
void gtk_spell_checker_add_word (GtkSpellChecker *self,
GtkSpellDictionary dictionary,
const char *word,
gssize word_length);
GDK_AVAILABLE_IN_4_2
void gtk_spell_checker_set_correction (GtkSpellChecker *self,
GtkSpellDictionary dictionary,
const char *word,
gssize word_length,
const char *correction,
gssize correction_length);
G_END_DECLS
#endif /* __GTK_SPELL_CHECK_H__ */

View File

@@ -0,0 +1,93 @@
/*
* Copyright 2021 Christian Hergert <chergert@redhat.com>
*
* 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 of the
* licence 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/>.
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#ifndef __GTK_SPELL_CHECK_PRIVATE_H__
#define __GTK_SPELL_CHECK_PRIVATE_H__
#include "gtkspellcheck.h"
G_BEGIN_DECLS
typedef struct _GtkSpellProvider GtkSpellProvider;
typedef struct _GtkSpellLanguage GtkSpellLanguage;
struct _GtkSpellProvider
{
const char *name;
gboolean (*supports) (const char *code);
char **(*list_languages) (void);
GListModel *(*list_corrections) (GtkSpellLanguage *language,
const char *word,
gssize word_length);
gboolean (*init_language) (GtkSpellLanguage *language);
void (*fini_language) (GtkSpellLanguage *language);
gboolean (*contains_word) (GtkSpellLanguage *language,
const char *word,
gssize word_length);
};
struct _GtkSpellLanguage
{
const GtkSpellProvider *provider;
char *code;
gpointer native;
};
struct _GtkSpellChecker
{
GObject parent_instance;
GPtrArray *languages;
};
struct _GtkSpellCorrection
{
GObject parent_instance;
char *text;
};
static inline gboolean
_gtk_spell_language_contains_word (GtkSpellLanguage *language,
const char *word,
gssize word_length)
{
return language->provider->contains_word (language, word, word_length);
}
static inline gboolean
_gtk_spell_checker_contains_word (GtkSpellChecker *checker,
const char *word,
gssize word_length)
{
if (word_length < 0)
word_length = strlen (word);
for (guint i = 0; i < checker->languages->len; i++)
{
GtkSpellLanguage *language = g_ptr_array_index (checker->languages, i);
if (_gtk_spell_language_contains_word (language, word, word_length))
return TRUE;
}
return FALSE;
}
G_END_DECLS
#endif /* __GTK_SPELL_CHECKER_PRIVATE_H__ */

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

1295
gtk/gtktextregion.c Normal file

File diff suppressed because it is too large Load Diff

556
gtk/gtktextregionbtree.h Normal file
View File

@@ -0,0 +1,556 @@
/* gtktextregionbtree.h
*
* Copyright 2021 Christian Hergert <chergert@redhat.com>
*
* This file 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 file 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 General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#ifndef __GTK_TEXT_REGION_BTREE_H__
#define __GTK_TEXT_REGION_BTREE_H__
#include "gtktextregionprivate.h"
G_BEGIN_DECLS
/* The following set of macros are used to create a queue similar to a
* double-ended linked list but using integers as indexes for items within the
* queue. Doing so allows for inserting or removing items from a b+tree node
* without having to memmove() data to maintain sorting orders.
*/
#define VAL_QUEUE_INVALID(Node) ((glib_typeof((Node)->head))-1)
#define VAL_QUEUE_LENGTH(Node) ((Node)->length)
#define VAL_QUEUE_EMPTY(Node) ((Node)->head == VAL_QUEUE_INVALID(Node))
#define VAL_QUEUE_PEEK_HEAD(Node) ((Node)->head)
#define VAL_QUEUE_PEEK_TAIL(Node) ((Node)->tail)
#define VAL_QUEUE_IS_VALID(Node, ID) ((ID) != VAL_QUEUE_INVALID(Node))
#define VAL_QUEUE_NODE(Type, N_Items) \
struct { \
Type length; \
Type head; \
Type tail; \
struct { \
Type prev; \
Type next; \
} items[N_Items]; \
}
#define VAL_QUEUE_INIT(Node) \
G_STMT_START { \
(Node)->length = 0; \
(Node)->head = VAL_QUEUE_INVALID(Node); \
(Node)->tail = VAL_QUEUE_INVALID(Node); \
for (guint _i = 0; _i < G_N_ELEMENTS ((Node)->items); _i++) \
{ \
(Node)->items[_i].next = VAL_QUEUE_INVALID(Node); \
(Node)->items[_i].prev = VAL_QUEUE_INVALID(Node); \
} \
} G_STMT_END
#ifndef G_DISABLE_ASSERT
# define _VAL_QUEUE_VALIDATE(Node) \
G_STMT_START { \
glib_typeof((Node)->head) count = 0; \
\
if ((Node)->tail != VAL_QUEUE_INVALID(Node)) \
g_assert_cmpint((Node)->items[(Node)->tail].next, ==, VAL_QUEUE_INVALID(Node)); \
if ((Node)->head != VAL_QUEUE_INVALID(Node)) \
g_assert_cmpint((Node)->items[(Node)->head].prev , ==, VAL_QUEUE_INVALID(Node)); \
\
for (glib_typeof((Node)->head) _viter = (Node)->head; \
VAL_QUEUE_IS_VALID(Node, _viter); \
_viter = (Node)->items[_viter].next) \
{ \
count++; \
} \
\
g_assert_cmpint(count, ==, (Node)->length); \
} G_STMT_END
#else
# define _VAL_QUEUE_VALIDATE(Node) G_STMT_START { } G_STMT_END
#endif
#define VAL_QUEUE_PUSH_HEAD(Node, ID) \
G_STMT_START { \
(Node)->items[ID].prev = VAL_QUEUE_INVALID(Node); \
(Node)->items[ID].next = (Node)->head; \
if (VAL_QUEUE_IS_VALID(Node, (Node)->head)) \
(Node)->items[(Node)->head].prev = ID; \
(Node)->head = ID; \
if (!VAL_QUEUE_IS_VALID(Node, (Node)->tail)) \
(Node)->tail = ID; \
(Node)->length++; \
_VAL_QUEUE_VALIDATE(Node); \
} G_STMT_END
#define VAL_QUEUE_PUSH_TAIL(Node, ID) \
G_STMT_START { \
(Node)->items[ID].prev = (Node)->tail; \
(Node)->items[ID].next = VAL_QUEUE_INVALID(Node); \
if (VAL_QUEUE_IS_VALID (Node, (Node)->tail)) \
(Node)->items[(Node)->tail].next = ID; \
(Node)->tail = ID; \
if (!VAL_QUEUE_IS_VALID(Node, (Node)->head)) \
(Node)->head = ID; \
(Node)->length++; \
_VAL_QUEUE_VALIDATE(Node); \
} G_STMT_END
#define VAL_QUEUE_INSERT(Node, Nth, Val) \
G_STMT_START { \
g_assert_cmpint (VAL_QUEUE_LENGTH(Node),<,G_N_ELEMENTS((Node)->items)); \
\
if ((Nth) == 0) \
{ \
VAL_QUEUE_PUSH_HEAD(Node, Val); \
} \
else if ((Nth) == (Node)->length) \
{ \
VAL_QUEUE_PUSH_TAIL(Node, Val); \
} \
else \
{ \
glib_typeof((Node)->head) ID; \
glib_typeof((Node)->head) _nth; \
\
g_assert_cmpint (VAL_QUEUE_LENGTH(Node), >, 0); \
g_assert (VAL_QUEUE_IS_VALID(Node, (Node)->head)); \
g_assert (VAL_QUEUE_IS_VALID(Node, (Node)->tail)); \
\
for (ID = (Node)->head, _nth = 0; \
_nth < (Nth) && VAL_QUEUE_IS_VALID(Node, ID); \
ID = (Node)->items[ID].next, ++_nth) \
{ /* Do Nothing */ } \
\
g_assert (VAL_QUEUE_IS_VALID(Node, ID)); \
g_assert (VAL_QUEUE_IS_VALID(Node, (Node)->items[ID].prev)); \
\
(Node)->items[Val].prev = (Node)->items[ID].prev; \
(Node)->items[Val].next = ID; \
(Node)->items[(Node)->items[ID].prev].next = Val; \
(Node)->items[ID].prev = Val; \
\
(Node)->length++; \
\
_VAL_QUEUE_VALIDATE(Node); \
} \
} G_STMT_END
#define VAL_QUEUE_POP_HEAD(Node,_pos) VAL_QUEUE_POP_NTH((Node), 0, _pos)
#define VAL_QUEUE_POP_TAIL(Node,_pos) VAL_QUEUE_POP_NTH((Node), (Node)->length - 1, _pos)
#define VAL_QUEUE_POP_AT(Node, _pos) \
G_STMT_START { \
g_assert (_pos != VAL_QUEUE_INVALID(Node)); \
g_assert (_pos < G_N_ELEMENTS ((Node)->items)); \
\
if ((Node)->items[_pos].prev != VAL_QUEUE_INVALID(Node)) \
(Node)->items[(Node)->items[_pos].prev].next = (Node)->items[_pos].next; \
if ((Node)->items[_pos].next != VAL_QUEUE_INVALID(Node)) \
(Node)->items[(Node)->items[_pos].next].prev = (Node)->items[_pos].prev; \
if ((Node)->head == _pos) \
(Node)->head = (Node)->items[_pos].next; \
if ((Node)->tail == _pos) \
(Node)->tail = (Node)->items[_pos].prev; \
\
(Node)->items[_pos].prev = VAL_QUEUE_INVALID((Node)); \
(Node)->items[_pos].next = VAL_QUEUE_INVALID((Node)); \
\
(Node)->length--; \
\
_VAL_QUEUE_VALIDATE(Node); \
} G_STMT_END
#define VAL_QUEUE_POP_NTH(Node, Nth, _pos) \
G_STMT_START { \
_pos = VAL_QUEUE_INVALID(Node); \
\
if (Nth == 0) \
_pos = (Node)->head; \
else if (Nth >= (((Node)->length) - 1)) \
_pos = (Node)->tail; \
else \
VAL_QUEUE_NTH (Node, Nth, _pos); \
\
if (_pos != VAL_QUEUE_INVALID(Node)) \
VAL_QUEUE_POP_AT (Node, _pos); \
} G_STMT_END
#define VAL_QUEUE_NTH(Node, Nth, _iter) \
G_STMT_START { \
glib_typeof((Node)->head) _nth; \
if (Nth == 0) \
_iter = (Node)->head; \
else if (Nth >= (((Node)->length) - 1)) \
_iter = (Node)->tail; \
else \
{ \
for (_iter = (Node)->head, _nth = 0; \
_nth < (Nth); \
_iter = (Node)->items[_iter].next, ++_nth) \
{ /* Do Nothing */ } \
} \
} G_STMT_END
#define _VAL_QUEUE_MOVE(Node, Old, New) \
G_STMT_START { \
(Node)->items[New] = (Node)->items[Old]; \
if ((Node)->items[New].prev != VAL_QUEUE_INVALID(Node)) \
(Node)->items[(Node)->items[New].prev].next = New; \
if ((Node)->items[New].next != VAL_QUEUE_INVALID(Node)) \
(Node)->items[(Node)->items[New].next].prev = New; \
if ((Node)->head == Old) \
(Node)->head = New; \
if ((Node)->tail == Old) \
(Node)->tail = New; \
} G_STMT_END
/*
* SORTED_ARRAY_FIELD:
* @TYPE: The type of the structure used by elements in the array
* @N_ITEMS: The maximum number of items in the array
*
* This creates a new inline structure that can be embedded within
* other super-structures.
*
* @N_ITEMS must be <= 254 or this macro will fail.
*/
#define SORTED_ARRAY_FIELD(TYPE,N_ITEMS) \
struct { \
TYPE items[N_ITEMS]; \
VAL_QUEUE_NODE(guint8, N_ITEMS) q; \
}
/*
* SORTED_ARRAY_INIT:
* @FIELD: A pointer to a SortedArray
*
* This will initialize a node that has been previously registered
* using %SORTED_ARRAY_FIELD(). You must call this macro before
* using the SortedArray structure.
*/
#define SORTED_ARRAY_INIT(FIELD) \
G_STMT_START { \
G_STATIC_ASSERT (G_N_ELEMENTS((FIELD)->items) < 255); \
VAL_QUEUE_INIT(&(FIELD)->q); \
} G_STMT_END
/*
* SORTED_ARRAY_LENGTH:
* @FIELD: A pointer to the SortedArray field.
*
* This macro will evaluate to the number of items inserted into
* the SortedArray.
*/
#define SORTED_ARRAY_LENGTH(FIELD) (VAL_QUEUE_LENGTH(&(FIELD)->q))
/*
* SORTED_ARRAY_CAPACITY:
* @FIELD: A pointer to the SortedArray field.
*
* This macro will evaluate to the number of elements in the SortedArray.
* This is dependent on how the SortedArray was instantiated using
* the %SORTED_ARRAY_FIELD() macro.
*/
#define SORTED_ARRAY_CAPACITY(FIELD) (G_N_ELEMENTS((FIELD)->items))
/*
* SORTED_ARRAY_IS_FULL:
* @FIELD: A pointer to the SortedArray field.
*
* This macro will evaluate to 1 if the SortedArray is at capacity.
* Otherwise, the macro will evaluate to 0.
*/
#define SORTED_ARRAY_IS_FULL(FIELD) (SORTED_ARRAY_LENGTH(FIELD) == SORTED_ARRAY_CAPACITY(FIELD))
/*
* SORTED_ARRAY_IS_EMPTY:
* @FIELD: A SortedArray field
*
* This macro will evaluate to 1 if the SortedArray contains zero children.
*/
#define SORTED_ARRAY_IS_EMPTY(FIELD) (SORTED_ARRAY_LENGTH(FIELD) == 0)
/*
* SORTED_ARRAY_INSERT_VAL:
* @FIELD: A pointer to a SortedArray field.
* @POSITION: the logical position at which to insert
* @ELEMENT: The element to insert
*
* This will insert a new item into the array. It is invalid API use
* to call this function while the SortedArray is at capacity. Check
* SORTED_ARRAY_IS_FULL() before using this function to be certain.
*/
#define SORTED_ARRAY_INSERT_VAL(FIELD,POSITION,ELEMENT) \
G_STMT_START { \
guint8 _pos; \
\
g_assert (POSITION >= 0); \
g_assert (POSITION <= SORTED_ARRAY_LENGTH(FIELD)); \
\
_pos = VAL_QUEUE_LENGTH(&(FIELD)->q); \
g_assert (_pos != VAL_QUEUE_INVALID(&(FIELD)->q)); \
(FIELD)->items[_pos] = ELEMENT; \
VAL_QUEUE_INSERT(&(FIELD)->q, POSITION, _pos); \
} G_STMT_END
#define SORTED_ARRAY_REMOVE_INDEX(FIELD,POSITION,_ele) \
G_STMT_START { \
guint8 _pos; \
guint8 _len; \
\
VAL_QUEUE_POP_NTH(&(FIELD)->q, POSITION, _pos); \
_ele = (FIELD)->items[_pos]; \
_len = VAL_QUEUE_LENGTH(&(FIELD)->q); \
\
/* We must preserve our invariant of having no empty gaps \
* in the array so that se can place new items always at the \
* end (to avoid scanning for an empty spot). \
* Therefore we move our tail item into the removed slot and \
* adjust the iqueue positions (which are all O(1). \
*/ \
\
if (_pos < _len) \
{ \
(FIELD)->items[_pos] = (FIELD)->items[_len]; \
_VAL_QUEUE_MOVE(&(FIELD)->q, _len, _pos); \
} \
} G_STMT_END
/* SORTED_ARRAY_FOREACH_REMOVE:
*
* This a form of SORTED_ARRAY_REMOVE_INDEX but to be used when you
* are within a SORTED_ARRAY_FOREACH() to avoid extra scanning.
*/
#define SORTED_ARRAY_FOREACH_REMOVE(FIELD) \
G_STMT_START { \
guint8 _pos = _current; \
guint8 _len = VAL_QUEUE_LENGTH(&(FIELD)->q); \
\
g_assert (_len > 0); \
g_assert (_pos < _len); \
VAL_QUEUE_POP_AT(&(FIELD)->q, _pos); \
g_assert (VAL_QUEUE_LENGTH(&(FIELD)->q) == _len-1); \
_len--; \
\
/* We must preserve our invariant of having no empty gaps \
* in the array so that se can place new items always at the \
* end (to avoid scanning for an empty spot). \
* Therefore we move our tail item into the removed slot and \
* adjust the iqueue positions (which are all O(1). \
*/ \
\
if (_pos < _len) \
{ \
(FIELD)->items[_pos] = (FIELD)->items[_len]; \
_VAL_QUEUE_MOVE(&(FIELD)->q, _len, _pos); \
\
/* We might need to change the iter if next position moved */ \
if (_aiter == _len) \
_aiter = _pos; \
} \
\
} G_STMT_END
/*
* SORTED_ARRAY_FOREACH:
* @FIELD: A pointer to a SortedArray
* @Element: The type of the elements in @FIELD
* @Name: the name for a pointer of type @Element
* @LABlock: a {} tyle block to execute for each item. You may use
* "break" to exit the foreach.
*
* Calls @Block for every element stored in @FIELD. A pointer to
* each element will be provided as a variable named @Name.
*/
#define SORTED_ARRAY_FOREACH(FIELD, Element, Name, LABlock) \
G_STMT_START { \
for (glib_typeof((FIELD)->q.head) _aiter = (FIELD)->q.head; \
_aiter != VAL_QUEUE_INVALID(&(FIELD)->q); \
/* Do Nothing */) \
{ \
G_GNUC_UNUSED glib_typeof((FIELD)->q.head) _current = _aiter; \
Element * Name = &(FIELD)->items[_aiter]; \
_aiter = (FIELD)->q.items[_aiter].next; \
LABlock \
} \
} G_STMT_END
#define SORTED_ARRAY_FOREACH_REVERSE(FIELD, Element, Name, LABlock) \
G_STMT_START { \
for (glib_typeof((FIELD)->q.head) _aiter = (FIELD)->q.tail; \
_aiter != VAL_QUEUE_INVALID(&(FIELD)->q); \
/* Do Nothing */) \
{ \
G_GNUC_UNUSED glib_typeof((FIELD)->q.head) _current = _aiter; \
Element * Name = &(FIELD)->items[_aiter]; \
_aiter = (FIELD)->q.items[_aiter].prev; \
LABlock \
} \
} G_STMT_END
#define SORTED_ARRAY_FOREACH_PEEK(FIELD) \
(((FIELD)->q.items[_current].next != VAL_QUEUE_INVALID(&(FIELD)->q)) \
? &(FIELD)->items[(FIELD)->q.items[_current].next] : NULL)
#define SORTED_ARRAY_SPLIT(FIELD, SPLIT) \
G_STMT_START { \
guint8 _mid; \
\
SORTED_ARRAY_INIT(SPLIT); \
\
_mid = SORTED_ARRAY_LENGTH(FIELD) / 2; \
\
for (guint8 _z = 0; _z < _mid; _z++) \
{ \
glib_typeof((FIELD)->items[0]) ele; \
SORTED_ARRAY_POP_TAIL(FIELD, ele); \
SORTED_ARRAY_PUSH_HEAD(SPLIT, ele); \
} \
} G_STMT_END
#define SORTED_ARRAY_SPLIT2(FIELD, LEFT, RIGHT) \
G_STMT_START { \
guint8 mid; \
\
SORTED_ARRAY_INIT(LEFT); \
SORTED_ARRAY_INIT(RIGHT); \
\
mid = SORTED_ARRAY_LENGTH(FIELD) / 2; \
\
for (guint8 i = 0; i < mid; i++) \
{ \
glib_typeof((FIELD)->items[0]) ele; \
SORTED_ARRAY_POP_TAIL(FIELD, ele); \
SORTED_ARRAY_PUSH_HEAD(RIGHT, ele); \
} \
\
while (!SORTED_ARRAY_IS_EMPTY(FIELD)) \
{ \
glib_typeof((FIELD)->items[0]) ele; \
SORTED_ARRAY_POP_TAIL(FIELD, ele); \
SORTED_ARRAY_PUSH_HEAD(LEFT, ele); \
} \
} G_STMT_END
#define SORTED_ARRAY_PEEK_HEAD(FIELD) ((FIELD)->items[VAL_QUEUE_PEEK_HEAD(&(FIELD)->q)])
#define SORTED_ARRAY_POP_HEAD(FIELD,_ele) SORTED_ARRAY_REMOVE_INDEX(FIELD, 0, _ele)
#define SORTED_ARRAY_POP_TAIL(FIELD,_ele) SORTED_ARRAY_REMOVE_INDEX(FIELD, SORTED_ARRAY_LENGTH(FIELD)-1, _ele)
#define SORTED_ARRAY_PUSH_HEAD(FIELD, ele) \
G_STMT_START { \
guint8 _pos = VAL_QUEUE_LENGTH(&(FIELD)->q); \
g_assert_cmpint (_pos, <, G_N_ELEMENTS ((FIELD)->items)); \
(FIELD)->items[_pos] = ele; \
VAL_QUEUE_PUSH_HEAD(&(FIELD)->q, _pos); \
} G_STMT_END
#define SORTED_ARRAY_PUSH_TAIL(FIELD, ele) \
G_STMT_START { \
guint8 _pos = VAL_QUEUE_LENGTH(&(FIELD)->q); \
g_assert_cmpint (_pos, <, G_N_ELEMENTS ((FIELD)->items)); \
(FIELD)->items[_pos] = ele; \
VAL_QUEUE_PUSH_TAIL(&(FIELD)->q, _pos); \
} G_STMT_END
#define GTK_TEXT_REGION_MAX_BRANCHES 26
#define GTK_TEXT_REGION_MIN_BRANCHES (GTK_TEXT_REGION_MAX_BRANCHES/3)
#define GTK_TEXT_REGION_MAX_RUNS 26
#define GTK_TEXT_REGION_MIN_RUNS (GTK_TEXT_REGION_MAX_RUNS/3)
typedef union _GtkTextRegionNode GtkTextRegionNode;
typedef struct _GtkTextRegionBranch GtkTextRegionBranch;
typedef struct _GtkTextRegionLeaf GtkTextRegionLeaf;
typedef struct _GtkTextRegionChild GtkTextRegionChild;
struct _GtkTextRegionChild
{
GtkTextRegionNode *node;
gsize length;
};
struct _GtkTextRegionBranch
{
GtkTextRegionNode *tagged_parent;
GtkTextRegionNode *prev;
GtkTextRegionNode *next;
SORTED_ARRAY_FIELD (GtkTextRegionChild, GTK_TEXT_REGION_MAX_BRANCHES) children;
};
struct _GtkTextRegionLeaf
{
GtkTextRegionNode *tagged_parent;
GtkTextRegionNode *prev;
GtkTextRegionNode *next;
SORTED_ARRAY_FIELD (GtkTextRegionRun, GTK_TEXT_REGION_MAX_RUNS) runs;
};
union _GtkTextRegionNode
{
/* pointer to the parent, low bit 0x1 means leaf node */
GtkTextRegionNode *tagged_parent;
struct _GtkTextRegionLeaf leaf;
struct _GtkTextRegionBranch branch;
};
struct _GtkTextRegion
{
GtkTextRegionNode root;
GtkTextRegionJoinFunc join_func;
GtkTextRegionSplitFunc split_func;
gsize length;
GtkTextRegionNode *cached_result;
gsize cached_result_offset;
};
#define TAG(ptr,val) GSIZE_TO_POINTER(GPOINTER_TO_SIZE(ptr)|(gsize)val)
#define UNTAG(ptr) GSIZE_TO_POINTER(GPOINTER_TO_SIZE(ptr) & ~(gsize)1)
static inline GtkTextRegionNode *
gtk_text_region_node_get_parent (GtkTextRegionNode *node)
{
if (node == NULL)
return NULL;
return UNTAG (node->tagged_parent);
}
static inline gboolean
gtk_text_region_node_is_leaf (GtkTextRegionNode *node)
{
GtkTextRegionNode *parent = gtk_text_region_node_get_parent (node);
return parent != NULL && node->tagged_parent != parent;
}
static inline void
gtk_text_region_node_set_parent (GtkTextRegionNode *node,
GtkTextRegionNode *parent)
{
node->tagged_parent = TAG (parent, gtk_text_region_node_is_leaf (node));
}
static inline gsize
gtk_text_region_node_length (GtkTextRegionNode *node)
{
gsize length = 0;
g_assert (node != NULL);
if (gtk_text_region_node_is_leaf (node))
{
SORTED_ARRAY_FOREACH (&node->leaf.runs, GtkTextRegionRun, run, {
length += run->length;
});
}
else
{
SORTED_ARRAY_FOREACH (&node->branch.children, GtkTextRegionChild, child, {
length += child->length;
});
}
return length;
}
static inline GtkTextRegionNode *
_gtk_text_region_get_first_leaf (GtkTextRegion *self)
{
for (GtkTextRegionNode *iter = &self->root;
iter;
iter = SORTED_ARRAY_PEEK_HEAD (&iter->branch.children).node)
{
if (gtk_text_region_node_is_leaf (iter))
return iter;
}
g_assert_not_reached ();
}
G_END_DECLS
#endif /* __GTK_TEXT_REGION_BTREE_H__ */

121
gtk/gtktextregionprivate.h Normal file
View File

@@ -0,0 +1,121 @@
/* gtktextregionprivate.h
*
* Copyright 2021 Christian Hergert <chergert@redhat.com>
*
* This file 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 file 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 General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#ifndef __GTK_TEXT_REGION_PRIVATE_H__
#define __GTK_TEXT_REGION_PRIVATE_H__
#include <glib.h>
G_BEGIN_DECLS
typedef struct _GtkTextRegion GtkTextRegion;
typedef struct _GtkTextRegionRun
{
gsize length;
gpointer data;
} GtkTextRegionRun;
/*
* GtkTextRegionForeachFunc:
* @offset: the offset in characters within the text region
* @run: the run of text and data pointer
* @user_data: user data supplied
*
* Function callback to iterate through runs within a text region.
*
* Returns: %FALSE to coninue iteration, otherwise %TRUE to stop.
*/
typedef gboolean (*GtkTextRegionForeachFunc) (gsize offset,
const GtkTextRegionRun *run,
gpointer user_data);
/*
* GtkTextRegionJoinFunc:
*
* This callback is used to determine if two runs can be joined together.
* This is useful when you have similar data pointers between two runs
* and seeing them as one run is irrelevant to the code using the
* text region.
*
* The default calllback for joining will return %FALSE so that no joins
* may occur.
*
* Returns: %TRUE if the runs can be joined; otherwise %FALSE
*/
typedef gboolean (*GtkTextRegionJoinFunc) (gsize offset,
const GtkTextRegionRun *left,
const GtkTextRegionRun *right);
/*
* GtkTextRegionSplitFunc:
*
* This function is responsible for splitting a run into two runs.
* This can happen a delete happens in the middle of a run.
*
* By default, @left will contain the run prior to the delete, and
* @right will contain the run after the delete.
*
* You can use the run lengths to determine where the delete was made
* using @offset which is an absolute offset from the beginning of the
* region.
*
* If you would like to keep a single run after the deletion, then
* set @right to contain a length of zero and add it's previous
* length to @left.
*
* All the length in @left and @right must be accounted for.
*
* This function is useful when using GtkTextRegion as a piecetable
* where you want to adjust the data pointer to point at a new
* section of an original or change buffer.
*/
typedef void (*GtkTextRegionSplitFunc) (gsize offset,
const GtkTextRegionRun *run,
GtkTextRegionRun *left,
GtkTextRegionRun *right);
GtkTextRegion *_gtk_text_region_new (GtkTextRegionJoinFunc join_func,
GtkTextRegionSplitFunc split_func);
void _gtk_text_region_insert (GtkTextRegion *region,
gsize offset,
gsize length,
gpointer data);
void _gtk_text_region_replace (GtkTextRegion *region,
gsize offset,
gsize length,
gpointer data);
void _gtk_text_region_remove (GtkTextRegion *region,
gsize offset,
gsize length);
guint _gtk_text_region_get_length (GtkTextRegion *region);
void _gtk_text_region_foreach (GtkTextRegion *region,
GtkTextRegionForeachFunc func,
gpointer user_data);
void _gtk_text_region_foreach_in_range (GtkTextRegion *region,
gsize begin,
gsize end,
GtkTextRegionForeachFunc func,
gpointer user_data);
void _gtk_text_region_free (GtkTextRegion *region);
G_END_DECLS
#endif /* __GTK_TEXT_REGION_PRIVATE_H__ */

View File

@@ -42,6 +42,7 @@
#include "gtktextiterprivate.h"
#include "gtkimmulticontext.h"
#include "gtkprivate.h"
#include "gtktextbufferprivate.h"
#include "gtktextutil.h"
#include "gtkwidgetprivate.h"
#include "gtkwindow.h"
@@ -55,6 +56,7 @@
#include "gtkemojichooser.h"
#include "gtkpango.h"
#include "gtknative.h"
#include "gtkspellcheck.h"
#include "gtkwidgetprivate.h"
/**
@@ -2207,6 +2209,8 @@ gtk_text_view_set_buffer (GtkTextView *text_view,
priv->first_para_pixels = 0;
/* XXX: remove me */
gtk_text_buffer_set_spell_checker (buffer, gtk_spell_checker_get_default ());
g_signal_connect (priv->buffer, "mark-set",
G_CALLBACK (gtk_text_view_mark_set_handler),
@@ -8068,6 +8072,26 @@ gtk_text_view_drag_drop (GtkDropTarget *dest,
return TRUE;
}
static void
gtk_text_view_check_spelling (GtkTextView *text_view)
{
GtkTextIter begin, end;
GdkRectangle visible;
g_assert (GTK_IS_TEXT_VIEW (text_view));
if (!_gtk_text_buffer_can_check_spelling (text_view->priv->buffer))
return;
gtk_text_view_get_visible_rect (text_view, &visible);
gtk_text_view_get_iter_at_location (text_view, &begin, visible.x, visible.y);
gtk_text_view_get_iter_at_location (text_view, &end,
visible.x + visible.width,
visible.y + visible.height);
_gtk_text_buffer_check_spelling (text_view->priv->buffer, &begin, &end);
}
static void
gtk_text_view_set_hadjustment (GtkTextView *text_view,
GtkAdjustment *adjustment)
@@ -8286,6 +8310,8 @@ gtk_text_view_value_changed (GtkAdjustment *adjustment,
gtk_text_view_update_handles (text_view);
gtk_text_view_check_spelling (text_view);
if (priv->anchored_children.length > 0)
gtk_widget_queue_allocate (GTK_WIDGET (text_view));
else

View File

@@ -145,6 +145,7 @@ gtk_private_sources = files([
'gtkstyleproperty.c',
'gtktextbtree.c',
'gtktexthistory.c',
'gtktextregion.c',
'gtktextviewchild.c',
'timsort/gtktimsort.c',
'gtktrashmonitor.c',
@@ -377,6 +378,7 @@ gtk_public_sources = files([
'gtksnapshot.c',
'gtksorter.c',
'gtksortlistmodel.c',
'gtkspellcheck.c',
'gtkspinbutton.c',
'gtkspinner.c',
'gtkstack.c',
@@ -648,6 +650,7 @@ gtk_public_headers = files([
'gtksnapshot.h',
'gtksorter.h',
'gtksortlistmodel.h',
'gtkspellcheck.h',
'gtkspinbutton.h',
'gtkspinner.h',
'gtkstack.h',
@@ -1001,6 +1004,7 @@ gtk_deps = [
epoxy_dep,
libm,
graphene_dep,
libenchant_dep,
]
if harfbuzz_dep.found() and pangoft_dep.found()

View File

@@ -23,6 +23,7 @@ epoxy_req = '>= 1.4'
cloudproviders_req = '>= 0.3.1'
xkbcommon_req = '>= 0.2.0'
sysprof_req = '>= 3.38.0'
enchant_req = '>= 2.2.0'
gnome = import('gnome')
pkg_config = import('pkgconfig')
@@ -667,6 +668,16 @@ else
profiler_enabled = false
endif
# enchant spellcheck support
spellcheck_backends = []
if not get_option('spell-enchant').disabled()
libenchant_dep = dependency('enchant-2', version: enchant_req, required: get_option('spell-enchant'))
if libenchant_dep.found()
spellcheck_backends += ['enchant2']
cdata.set10('HAVE_ENCHANT', true)
endif
endif
graphene_dep_type = graphene_dep.type_name()
if graphene_dep_type == 'pkgconfig'
graphene_has_sse2 = graphene_dep.get_pkgconfig_variable('graphene_has_sse2') == '1'
@@ -834,6 +845,7 @@ endif
summary('Display backends', display_backends)
summary('Print backends', print_backends)
summary('Media backends', media_backends)
summary('Spell backends', spellcheck_backends)
summary('Vulkan support', vulkan_dep.found(), section: 'Features')
summary('Cloud support', cloudproviders_dep.found(), section: 'Features')

View File

@@ -49,6 +49,13 @@ option('print-cloudprint',
value: 'auto',
description : 'Build the cloudprint print backend')
# Spell Check Providers
option('spell-enchant',
type: 'feature',
value: 'auto',
description : 'Build the enchant spellcheck backend')
# Optional features
option('vulkan',

View File

@@ -68,6 +68,7 @@ gtk_tests = [
['testscale'],
['testselectionmode'],
['testsounds'],
['testspelling'],
['testspinbutton'],
['testtreechanging'],
['testtreednd'],

68
tests/testspelling.c Normal file
View File

@@ -0,0 +1,68 @@
/*
* Copyright 2021 Christian Hergert <chergert@redhat.com>
*
* 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 of the
* licence 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/>.
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include <gtk/gtk.h>
int
main (int argc, char *argv[])
{
GtkSpellChecker *checker;
gtk_init ();
checker = gtk_spell_checker_get_default ();
for (guint i = 1; i < argc; i++)
{
const char *word = argv[i];
GListModel *corrections;
guint n_items = 0;
if (gtk_spell_checker_contains_word (checker, word, -1))
{
g_print ("Dictionary contains the word “%s”\n", word);
continue;
}
if ((corrections = gtk_spell_checker_list_corrections (checker, word, -1)))
{
n_items = g_list_model_get_n_items (G_LIST_MODEL (corrections));
if (n_items > 0)
g_print ("Corrections for “%s”:\n", word);
for (guint j = 0; j < n_items; j++)
{
GtkStringObject *correction = g_list_model_get_item (corrections, j);
const char *text = gtk_string_object_get_string (correction);
g_print (" %s\n", text);
}
g_object_unref (corrections);
}
if (n_items == 0)
g_print ("No corrections for “%s” were found.\n", word);
}
g_assert_finalize_object (checker);
return 0;
}

View File

@@ -115,6 +115,7 @@ internal_tests = [
{ 'name': 'rbtree-crash' },
{ 'name': 'propertylookuplistmodel' },
{ 'name': 'rbtree' },
{ 'name': 'textregion' },
{ 'name': 'timsort' },
]

631
testsuite/gtk/textregion.c Normal file
View File

@@ -0,0 +1,631 @@
#include "gtktextregionprivate.h"
#include "gtktextregionbtree.h"
static void
assert_leaves_empty (GtkTextRegion *region)
{
GtkTextRegionNode *leaf = _gtk_text_region_get_first_leaf (region);
guint count = 0;
for (; leaf; leaf = leaf->leaf.next, count++)
{
GtkTextRegionNode *parent = gtk_text_region_node_get_parent (leaf);
guint length = gtk_text_region_node_length (leaf);
guint length_in_parent = 0;
SORTED_ARRAY_FOREACH (&parent->branch.children, GtkTextRegionChild, child, {
if (child->node == leaf)
{
length_in_parent = child->length;
break;
}
});
if (length || length_in_parent)
g_error ("leaf %p %u has length of %u in %u runs. Parent thinks it has length of %u.",
leaf, count, length, SORTED_ARRAY_LENGTH (&leaf->leaf.runs), length_in_parent);
}
}
static guint
count_leaves (GtkTextRegion *region)
{
GtkTextRegionNode *leaf = _gtk_text_region_get_first_leaf (region);
guint count = 0;
for (; leaf; leaf = leaf->leaf.next)
count++;
return count;
}
static guint
count_internal_recuse (GtkTextRegionNode *node)
{
guint count = 1;
g_assert (!gtk_text_region_node_is_leaf (node));
SORTED_ARRAY_FOREACH (&node->branch.children, GtkTextRegionChild, child, {
g_assert (child->node != NULL);
if (!gtk_text_region_node_is_leaf (child->node))
count += count_internal_recuse (child->node);
});
return count;
}
static guint
count_internal (GtkTextRegion *region)
{
return count_internal_recuse (&region->root);
}
G_GNUC_UNUSED static inline void
print_tree (GtkTextRegionNode *node,
guint depth)
{
for (guint i = 0; i < depth; i++)
g_print (" ");
g_print ("%p %s Length=%"G_GSIZE_MODIFIER"u Items=%u Prev<%p> Next<%p>\n",
node,
gtk_text_region_node_is_leaf (node) ? "Leaf" : "Branch",
gtk_text_region_node_length (node),
gtk_text_region_node_is_leaf (node) ?
SORTED_ARRAY_LENGTH (&node->leaf.runs) :
SORTED_ARRAY_LENGTH (&node->branch.children),
gtk_text_region_node_is_leaf (node) ? node->leaf.prev : node->branch.prev,
gtk_text_region_node_is_leaf (node) ? node->leaf.next : node->branch.next);
if (!gtk_text_region_node_is_leaf (node))
{
SORTED_ARRAY_FOREACH (&node->branch.children, GtkTextRegionChild, child, {
print_tree (child->node, depth+1);
});
}
}
static void
assert_empty (GtkTextRegion *region)
{
#if 0
print_tree (&region->root, 0);
#endif
g_assert_cmpint (_gtk_text_region_get_length (region), ==, 0);
assert_leaves_empty (region);
g_assert_cmpint (1, ==, count_internal (region));
g_assert_cmpint (1, ==, count_leaves (region));
}
static gboolean
non_overlapping_insert_remove_cb (gsize offset,
const GtkTextRegionRun *run,
gpointer user_data)
{
g_assert_cmpint (offset, ==, GPOINTER_TO_UINT (run->data));
return FALSE;
}
static void
non_overlapping_insert_remove (void)
{
GtkTextRegion *region = _gtk_text_region_new (NULL, NULL);
assert_empty (region);
for (guint i = 0; i < 100000; i++)
{
_gtk_text_region_insert (region, i, 1, GUINT_TO_POINTER (i));
g_assert_cmpint (_gtk_text_region_get_length (region), ==, i + 1);
}
g_assert_cmpint (_gtk_text_region_get_length (region), ==, 100000);
_gtk_text_region_foreach (region, non_overlapping_insert_remove_cb, NULL);
for (guint i = 0; i < 100000; i++)
_gtk_text_region_remove (region, 100000-1-i, 1);
g_assert_cmpint (_gtk_text_region_get_length (region), ==, 0);
assert_empty (region);
_gtk_text_region_free (region);
}
typedef struct {
gsize offset;
gsize length;
gpointer data;
} SplitRunCheck;
typedef struct {
gsize index;
gsize count;
const SplitRunCheck *checks;
} SplitRun;
static gboolean
split_run_cb (gsize offset,
const GtkTextRegionRun *run,
gpointer user_data)
{
SplitRun *state = user_data;
g_assert_cmpint (offset, ==, state->checks[state->index].offset);
g_assert_cmpint (run->length, ==, state->checks[state->index].length);
g_assert_true (run->data == state->checks[state->index].data);
state->index++;
return FALSE;
}
static void
split_run (void)
{
static const SplitRunCheck checks[] = {
{ 0, 1, NULL },
{ 1, 1, GSIZE_TO_POINTER (1) },
{ 2, 1, NULL },
};
SplitRun state = { 0, 3, checks };
GtkTextRegion *region = _gtk_text_region_new (NULL, NULL);
_gtk_text_region_insert (region, 0, 2, NULL);
g_assert_cmpint (_gtk_text_region_get_length (region), ==, 2);
_gtk_text_region_insert (region, 1, 1, GSIZE_TO_POINTER (1));
g_assert_cmpint (_gtk_text_region_get_length (region), ==, 3);
_gtk_text_region_foreach (region, split_run_cb, &state);
_gtk_text_region_free (region);
}
static gboolean
can_join_cb (gsize offset,
const GtkTextRegionRun *left,
const GtkTextRegionRun *right)
{
return left->data == right->data;
}
static void
no_split_run (void)
{
static const SplitRunCheck checks[] = {
{ 0, 3, NULL },
};
SplitRun state = { 0, 1, checks };
GtkTextRegion *region = _gtk_text_region_new (can_join_cb, NULL);
_gtk_text_region_insert (region, 0, 2, NULL);
g_assert_cmpint (_gtk_text_region_get_length (region), ==, 2);
_gtk_text_region_insert (region, 1, 1, NULL);
g_assert_cmpint (_gtk_text_region_get_length (region), ==, 3);
_gtk_text_region_foreach (region, split_run_cb, &state);
_gtk_text_region_free (region);
}
static void
random_insertion (void)
{
GtkTextRegion *region = _gtk_text_region_new (NULL, NULL);
gsize expected = 0;
for (guint i = 0; i < 10000; i++)
{
guint pos = g_random_int_range (0, region->length + 1);
guint len = g_random_int_range (1, 20);
_gtk_text_region_insert (region, pos, len, GUINT_TO_POINTER (i));
expected += len;
}
g_assert_cmpint (expected, ==, region->length);
_gtk_text_region_replace (region, 0, region->length, NULL);
g_assert_cmpint (count_leaves (region), ==, 1);
g_assert_cmpint (count_internal (region), ==, 1);
_gtk_text_region_free (region);
}
static void
random_deletion (void)
{
GtkTextRegion *region = _gtk_text_region_new (NULL, NULL);
_gtk_text_region_insert (region, 0, 10000, NULL);
while (region->length > 0)
{
guint pos = region->length > 1 ? g_random_int_range (0, region->length-1) : 0;
guint len = region->length - pos > 1 ? g_random_int_range (1, region->length - pos) : 1;
_gtk_text_region_remove (region, pos, len);
}
_gtk_text_region_free (region);
}
static void
random_insert_deletion (void)
{
GtkTextRegion *region = _gtk_text_region_new (NULL, NULL);
guint expected = 0;
guint i = 0;
while (region->length < 10000)
{
guint pos = g_random_int_range (0, region->length + 1);
guint len = g_random_int_range (1, 20);
_gtk_text_region_insert (region, pos, len, GUINT_TO_POINTER (i));
expected += len;
i++;
}
g_assert_cmpint (expected, ==, region->length);
while (region->length > 0)
{
guint pos = region->length > 1 ? g_random_int_range (0, region->length-1) : 0;
guint len = region->length - pos > 1 ? g_random_int_range (1, region->length - pos) : 1;
g_assert (pos + len <= region->length);
_gtk_text_region_remove (region, pos, len);
}
_gtk_text_region_free (region);
}
static void
test_val_queue (void)
{
VAL_QUEUE_NODE(guint8, 32) field;
guint8 pos;
VAL_QUEUE_INIT (&field);
for (guint i = 0; i < 32; i++)
VAL_QUEUE_PUSH_TAIL (&field, i);
g_assert_cmpint (VAL_QUEUE_LENGTH (&field), ==, 32);
for (guint i = 0; i < 32; i++)
{
VAL_QUEUE_NTH (&field, i, pos);
g_assert_cmpint (pos, ==, i);
}
for (guint i = 0; i < 32; i++)
{
VAL_QUEUE_POP_HEAD (&field, pos);
g_assert_cmpint (pos, ==, i);
}
g_assert_cmpint (VAL_QUEUE_LENGTH (&field), ==, 0);
for (guint i = 0; i < 32; i++)
VAL_QUEUE_PUSH_TAIL (&field, i);
g_assert_cmpint (VAL_QUEUE_LENGTH (&field), ==, 32);
for (guint i = 0; i < 32; i++)
{
VAL_QUEUE_POP_TAIL (&field, pos);
g_assert_cmpint (pos, ==, 31-i);
}
g_assert_cmpint (VAL_QUEUE_LENGTH (&field), ==, 0);
for (guint i = 0; i < 32; i++)
VAL_QUEUE_PUSH_TAIL (&field, i);
while (VAL_QUEUE_LENGTH (&field))
VAL_QUEUE_POP_NTH (&field, VAL_QUEUE_LENGTH (&field)/2, pos);
}
typedef struct {
int v;
} Dummy;
static void
sorted_array (void)
{
SORTED_ARRAY_FIELD (Dummy, 32) field;
Dummy d;
guint i;
SORTED_ARRAY_INIT (&field);
d.v = 0; SORTED_ARRAY_INSERT_VAL (&field, 0, d);
d.v = 2; SORTED_ARRAY_INSERT_VAL (&field, 1, d);
d.v = 1; SORTED_ARRAY_INSERT_VAL (&field, 1, d);
i = 0;
g_assert_cmpint (SORTED_ARRAY_LENGTH (&field), ==, 3);
SORTED_ARRAY_FOREACH (&field, Dummy, dummy, {
g_assert_cmpint (dummy->v, ==, i++);
});
g_assert_cmpint (i, ==, 3);
SORTED_ARRAY_POP_HEAD (&field, d); g_assert_cmpint (d.v, ==, 0);
SORTED_ARRAY_POP_HEAD (&field, d); g_assert_cmpint (d.v, ==, 1);
SORTED_ARRAY_POP_HEAD (&field, d); g_assert_cmpint (d.v, ==, 2);
for (i = 0; i < 10; i++)
{ d.v = i * 2;
SORTED_ARRAY_INSERT_VAL (&field, i, d); }
for (i = 0; i < 10; i++)
{ d.v = i * 2 + 1;
SORTED_ARRAY_INSERT_VAL (&field, i*2+1, d); }
i = 0;
g_assert_cmpint (SORTED_ARRAY_LENGTH (&field), ==, 20);
SORTED_ARRAY_FOREACH (&field, Dummy, dummy, {
g_assert_cmpint (dummy->v, ==, i++);
});
g_assert_cmpint (i, ==, 20);
SORTED_ARRAY_FOREACH (&field, Dummy, dummy, {
(void)dummy;
SORTED_ARRAY_FOREACH_REMOVE (&field);
});
g_assert_cmpint (SORTED_ARRAY_LENGTH (&field), ==, 0);
for (i = 0; i < 32; i++)
{
d.v = i;
SORTED_ARRAY_PUSH_TAIL (&field, d);
}
g_assert_cmpint (32, ==, SORTED_ARRAY_LENGTH (&field));
i = 0;
SORTED_ARRAY_FOREACH (&field, Dummy, dummy, {
g_assert_cmpint (dummy->v, ==, i);
g_assert_cmpint (SORTED_ARRAY_LENGTH (&field), ==, 32-i);
SORTED_ARRAY_FOREACH_REMOVE (&field);
i++;
});
g_assert_cmpint (0, ==, SORTED_ARRAY_LENGTH (&field));
for (i = 0; i < 32; i++)
{
d.v = i;
SORTED_ARRAY_PUSH_TAIL (&field, d);
}
g_assert_cmpint (32, ==, SORTED_ARRAY_LENGTH (&field));
i = 31;
SORTED_ARRAY_FOREACH_REVERSE (&field, Dummy, dummy, {
g_assert_cmpint (dummy->v, ==, i);
SORTED_ARRAY_REMOVE_INDEX (&field, i, d);
i--;
});
}
static gboolean
replace_part_of_long_run_join (gsize offset,
const GtkTextRegionRun *left,
const GtkTextRegionRun *right)
{
return FALSE;
}
static void
replace_part_of_long_run_split (gsize offset,
const GtkTextRegionRun *run,
GtkTextRegionRun *left,
GtkTextRegionRun *right)
{
left->data = run->data;
right->data = GSIZE_TO_POINTER (GPOINTER_TO_SIZE (run->data) + left->length);
}
static void
replace_part_of_long_run (void)
{
GtkTextRegion *region = _gtk_text_region_new (replace_part_of_long_run_join,
replace_part_of_long_run_split);
static const SplitRunCheck checks0[] = {
{ 0, 5, NULL },
};
static const SplitRunCheck checks1[] = {
{ 0, 1, NULL },
{ 1, 3, GSIZE_TO_POINTER (2) },
};
static const SplitRunCheck checks2[] = {
{ 0, 1, GSIZE_TO_POINTER (0) },
{ 1, 1, GSIZE_TO_POINTER ((1L<<31)|1) },
{ 2, 3, GSIZE_TO_POINTER (2) },
};
static const SplitRunCheck checks3[] = {
{ 0, 1, GSIZE_TO_POINTER (0) },
{ 1, 1, GSIZE_TO_POINTER ((1L<<31)|1) },
{ 2, 1, GSIZE_TO_POINTER (2) },
{ 3, 1, GSIZE_TO_POINTER (4) },
};
static const SplitRunCheck checks4[] = {
{ 0, 1, GSIZE_TO_POINTER (0) },
{ 1, 1, GSIZE_TO_POINTER ((1L<<31)|1) },
{ 2, 1, GSIZE_TO_POINTER (2) },
{ 3, 1, GSIZE_TO_POINTER ((1L<<31)|2) },
{ 4, 1, GSIZE_TO_POINTER (4) },
};
SplitRun state0 = { 0, 1, checks0 };
SplitRun state1 = { 0, 2, checks1 };
SplitRun state2 = { 0, 3, checks2 };
SplitRun state3 = { 0, 4, checks3 };
SplitRun state4 = { 0, 5, checks4 };
_gtk_text_region_insert (region, 0, 5, NULL);
_gtk_text_region_foreach (region, split_run_cb, &state0);
_gtk_text_region_remove (region, 1, 1);
_gtk_text_region_foreach (region, split_run_cb, &state1);
_gtk_text_region_insert (region, 1, 1, GSIZE_TO_POINTER ((1L<<31)|1));
_gtk_text_region_foreach (region, split_run_cb, &state2);
_gtk_text_region_remove (region, 3, 1);
_gtk_text_region_foreach (region, split_run_cb, &state3);
_gtk_text_region_insert (region, 3, 1, GSIZE_TO_POINTER ((1L<<31)|2));
_gtk_text_region_foreach (region, split_run_cb, &state4);
_gtk_text_region_free (region);
}
typedef struct
{
char *original;
char *changes;
GString *res;
} wordstate;
static gboolean
word_foreach_cb (gsize offset,
const GtkTextRegionRun *run,
gpointer data)
{
wordstate *state = data;
gsize sdata = GPOINTER_TO_SIZE (run->data);
gsize soff = sdata & ~(1L<<31);
char *src;
if (sdata == soff)
src = state->original;
else
src = state->changes;
#if 0
g_print ("%lu len %lu (%s at %lu) %s\n",
offset, run->length, sdata == soff ? "original" : "changes", soff,
sdata == soff && src[sdata] == '\n' ? "is-newline" : "");
#endif
g_string_append_len (state->res, src + soff, run->length);
return FALSE;
}
static gboolean
join_word_cb (gsize offset,
const GtkTextRegionRun *left,
const GtkTextRegionRun *right)
{
return FALSE;
}
static void
split_word_cb (gsize offset,
const GtkTextRegionRun *run,
GtkTextRegionRun *left,
GtkTextRegionRun *right)
{
gsize sdata = GPOINTER_TO_SIZE (run->data);
left->data = run->data;
right->data = GSIZE_TO_POINTER (sdata + left->length);
}
static void
test_words_database (void)
{
GtkTextRegion *region = _gtk_text_region_new (join_word_cb, split_word_cb);
g_autofree char *contents = NULL;
g_autoptr(GString) str = g_string_new (NULL);
g_autoptr(GString) res = g_string_new (NULL);
const char *word;
const char *iter;
gsize len;
wordstate state;
if (!g_file_get_contents ("/usr/share/dict/words", &contents, &len, NULL))
{
g_test_skip ("Words database not available");
return;
}
/* 0 offset of base buffer */
_gtk_text_region_insert (region, 0, len, NULL);
/* For each each word, remove it and replace it with a word added to str.
* At the end we'll create the buffer and make sure we get the same.
*/
word = contents;
iter = contents;
for (;;)
{
if (*iter == 0)
break;
if (g_unichar_isspace (g_utf8_get_char (iter)))
{
gsize pos = str->len;
g_string_append_len (str, word, iter - word);
_gtk_text_region_replace (region, word - contents, iter - word, GSIZE_TO_POINTER ((1L<<31)|pos));
while (*iter && g_unichar_isspace (g_utf8_get_char (iter)))
iter = g_utf8_next_char (iter);
word = iter;
}
else
iter = g_utf8_next_char (iter);
}
state.original = contents;
state.changes = str->str;
state.res = res;
_gtk_text_region_foreach (region, word_foreach_cb, &state);
g_assert_true (g_str_equal (contents, res->str));
_gtk_text_region_free (region);
}
static gboolean
foreach_cb (gsize offset,
const GtkTextRegionRun *run,
gpointer user_data)
{
guint *count = user_data;
g_assert_cmpint (GPOINTER_TO_SIZE (run->data), ==, offset);
(*count)++;
return FALSE;
}
static void
foreach_in_range (void)
{
GtkTextRegion *region = _gtk_text_region_new (NULL, NULL);
guint count;
for (guint i = 0; i < 100000; i++)
{
_gtk_text_region_insert (region, i, 1, GUINT_TO_POINTER (i));
g_assert_cmpint (_gtk_text_region_get_length (region), ==, i + 1);
}
count = 0;
_gtk_text_region_foreach_in_range (region, 0, 100000, foreach_cb, &count);
g_assert_cmpint (count, ==, 100000);
count = 0;
_gtk_text_region_foreach_in_range (region, 1000, 5000, foreach_cb, &count);
g_assert_cmpint (count, ==, 4000);
_gtk_text_region_replace (region, 0, 10000, NULL);
count = 0;
_gtk_text_region_foreach_in_range (region, 1000, 5000, foreach_cb, &count);
g_assert_cmpint (count, ==, 1);
_gtk_text_region_free (region);
}
int
main (int argc,
char *argv[])
{
g_test_init (&argc, &argv, NULL);
g_test_add_func ("/Gtk/TextRegion/val_queue", test_val_queue);
g_test_add_func ("/Gtk/TextRegion/sorted_array", sorted_array);
g_test_add_func ("/Gtk/TextRegion/non_overlapping_insert_remove", non_overlapping_insert_remove);
g_test_add_func ("/Gtk/TextRegion/foreach_in_range", foreach_in_range);
g_test_add_func ("/Gtk/TextRegion/split_run", split_run);
g_test_add_func ("/Gtk/TextRegion/no_split_run", no_split_run);
g_test_add_func ("/Gtk/TextRegion/random_insertion", random_insertion);
g_test_add_func ("/Gtk/TextRegion/random_deletion", random_deletion);
g_test_add_func ("/Gtk/TextRegion/random_insert_deletion", random_insert_deletion);
g_test_add_func ("/Gtk/TextRegion/replace_part_of_long_run", replace_part_of_long_run);
g_test_add_func ("/Gtk/TextRegion/words_database", test_words_database);
return g_test_run ();
}