Compare commits
16 Commits
css-variab
...
wip/cherge
Author | SHA1 | Date | |
---|---|---|---|
|
dd7ce2e1ac | ||
|
1715f77868 | ||
|
c68eb0a064 | ||
|
a35cd721bb | ||
|
a24281e900 | ||
|
5e3f1ee42a | ||
|
1264245601 | ||
|
3f6b2d9b9f | ||
|
cbd29b4ce9 | ||
|
f25e53bd45 | ||
|
cb86fcc6d5 | ||
|
c77de7d328 | ||
|
71a317b01e | ||
|
78f970175d | ||
|
f390043e90 | ||
|
15abded486 |
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -53,6 +53,8 @@ struct _GdkWaylandKeymap
|
||||
|
||||
PangoDirection *direction;
|
||||
gboolean bidi;
|
||||
|
||||
char **languages;
|
||||
};
|
||||
|
||||
struct _GdkWaylandKeymapClass
|
||||
|
@@ -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
543
gtk/gtkspellcheck.c
Normal 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
69
gtk/gtkspellcheck.h
Normal 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__ */
|
93
gtk/gtkspellcheckprivate.h
Normal file
93
gtk/gtkspellcheckprivate.h
Normal 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__ */
|
@@ -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
|
||||
|
||||
|
1295
gtk/gtktextregion.c
Normal file
1295
gtk/gtktextregion.c
Normal file
File diff suppressed because it is too large
Load Diff
556
gtk/gtktextregionbtree.h
Normal file
556
gtk/gtktextregionbtree.h
Normal 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
121
gtk/gtktextregionprivate.h
Normal 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__ */
|
@@ -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
|
||||
|
@@ -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()
|
||||
|
12
meson.build
12
meson.build
@@ -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')
|
||||
|
@@ -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',
|
||||
|
@@ -68,6 +68,7 @@ gtk_tests = [
|
||||
['testscale'],
|
||||
['testselectionmode'],
|
||||
['testsounds'],
|
||||
['testspelling'],
|
||||
['testspinbutton'],
|
||||
['testtreechanging'],
|
||||
['testtreednd'],
|
||||
|
68
tests/testspelling.c
Normal file
68
tests/testspelling.c
Normal 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;
|
||||
}
|
@@ -115,6 +115,7 @@ internal_tests = [
|
||||
{ 'name': 'rbtree-crash' },
|
||||
{ 'name': 'propertylookuplistmodel' },
|
||||
{ 'name': 'rbtree' },
|
||||
{ 'name': 'textregion' },
|
||||
{ 'name': 'timsort' },
|
||||
]
|
||||
|
||||
|
631
testsuite/gtk/textregion.c
Normal file
631
testsuite/gtk/textregion.c
Normal 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 (®ion->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 (®ion->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 ();
|
||||
}
|
Reference in New Issue
Block a user