/*
* This file is part of gspell, a spell-checking library.
*
* Copyright 2015, 2016, 2017 - Sébastien Wilmet
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this library; if not, see <http://www.gnu.org/licenses/>.
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include "gspell-text-view.h"
#include <glib/gi18n-lib.h>
#include "gspell-inline-checker-text-buffer.h"
#include "gspell-checker.h"
#include "gspell-language.h"
#include "gspell-text-buffer.h"
#include "gspell-context-menu.h"
/**
* SECTION:text-view
* @Title: GspellTextView
* @Short_description: Spell checking support for GtkTextView
*
* #GspellTextView extends the #GtkTextView class with inline spell checking.
* Misspelled words are highlighted with a red %PANGO_UNDERLINE_SINGLE.
* Right-clicking a misspelled word pops up a context menu of suggested
* replacements. The context menu also contains an “Ignore All” item to add the
* misspelled word to the session dictionary. And an “Add” item to add the word
* to the personal dictionary.
*
* For a basic use-case, there is the gspell_text_view_basic_setup() convenience
* function.
*
* The spell is checked only on the visible region of the #GtkTextView. Note
* that if a same #GtkTextBuffer is used for several views, the misspelled words
* are visible in all views, because the highlighting is achieved with a
* #GtkTextTag added to the buffer.
*
* If you don't use the gspell_text_view_basic_setup() function, you need to
* call gspell_text_buffer_set_spell_checker() to associate a #GspellChecker to
* the #GtkTextBuffer.
*
* Note that #GspellTextView extends the #GtkTextView class but without
* subclassing it, because the GtkSourceView library has already a #GtkTextView
* subclass.
*
* If you want a %PANGO_UNDERLINE_ERROR instead (a wavy underline), please fix
* [this bug](https://bugzilla.gnome.org/show_bug.cgi?id=763741) first.
*/
typedef struct _GspellTextViewPrivate GspellTextViewPrivate;
struct _GspellTextViewPrivate
{
GtkTextView *view;
GspellInlineCheckerTextBuffer *inline_checker;
guint enable_language_menu : 1;
};
enum
{
PROP_0,
PROP_VIEW,
PROP_INLINE_SPELL_CHECKING,
PROP_ENABLE_LANGUAGE_MENU,
};
#define GSPELL_TEXT_VIEW_KEY "gspell-text-view-key"
G_DEFINE_TYPE_WITH_PRIVATE (GspellTextView, gspell_text_view, G_TYPE_OBJECT)
static void
create_inline_checker (GspellTextView *gspell_view)
{
GspellTextViewPrivate *priv;
GtkTextBuffer *buffer;
priv = gspell_text_view_get_instance_private (gspell_view);
if (priv->inline_checker != NULL)
{
return;
}
buffer = gtk_text_view_get_buffer (priv->view);
priv->inline_checker = _gspell_inline_checker_text_buffer_new (buffer);
_gspell_inline_checker_text_buffer_attach_view (priv->inline_checker,
priv->view);
}
static void
destroy_inline_checker (GspellTextView *gspell_view)
{
GspellTextViewPrivate *priv;
priv = gspell_text_view_get_instance_private (gspell_view);
if (priv->view == NULL || priv->inline_checker == NULL)
{
return;
}
_gspell_inline_checker_text_buffer_detach_view (priv->inline_checker,
priv->view);
g_clear_object (&priv->inline_checker);
}
static void
notify_buffer_cb (GtkTextView *gtk_view,
GParamSpec *pspec,
GspellTextView *gspell_view)
{
GspellTextViewPrivate *priv;
priv = gspell_text_view_get_instance_private (gspell_view);
if (priv->inline_checker == NULL)
{
return;
}
destroy_inline_checker (gspell_view);
create_inline_checker (gspell_view);
}
static void
language_activated_cb (const GspellLanguage *lang,
gpointer user_data)
{
GspellTextView *gspell_view;
GspellTextViewPrivate *priv;
GtkTextBuffer *gtk_buffer;
GspellTextBuffer *gspell_buffer;
GspellChecker *checker;
g_return_if_fail (GSPELL_IS_TEXT_VIEW (user_data));
gspell_view = GSPELL_TEXT_VIEW (user_data);
priv = gspell_text_view_get_instance_private (gspell_view);
gtk_buffer = gtk_text_view_get_buffer (priv->view);
gspell_buffer = gspell_text_buffer_get_from_gtk_text_buffer (gtk_buffer);
checker = gspell_text_buffer_get_spell_checker (gspell_buffer);
gspell_checker_set_language (checker, lang);
}
static const GspellLanguage *
get_current_language (GspellTextView *gspell_view)
{
GspellTextViewPrivate *priv;
GtkTextBuffer *gtk_buffer;
GspellTextBuffer *gspell_buffer;
GspellChecker *checker;
priv = gspell_text_view_get_instance_private (gspell_view);
if (priv->view == NULL)
{
return NULL;
}
gtk_buffer = gtk_text_view_get_buffer (priv->view);
gspell_buffer = gspell_text_buffer_get_from_gtk_text_buffer (gtk_buffer);
checker = gspell_text_buffer_get_spell_checker (gspell_buffer);
return gspell_checker_get_language (checker);
}
static void
populate_popup_cb (GtkTextView *gtk_view,
GtkWidget *popup,
GspellTextView *gspell_view)
{
GspellTextViewPrivate *priv;
GtkMenu *menu;
GtkWidget *menu_item;
priv = gspell_text_view_get_instance_private (gspell_view);
if (!GTK_IS_MENU (popup))
{
return;
}
menu = GTK_MENU (popup);
if (!priv->enable_language_menu &&
priv->inline_checker == NULL)
{
return;
}
/* Prepend separator */
menu_item = gtk_separator_menu_item_new ();
gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), menu_item);
gtk_widget_show (menu_item);
if (priv->enable_language_menu)
{
const GspellLanguage *current_language;
GtkMenuItem *lang_menu_item;
current_language = get_current_language (gspell_view);
lang_menu_item = _gspell_context_menu_get_language_menu_item (current_language,
language_activated_cb,
gspell_view);
/* Prepend language sub-menu */
gtk_menu_shell_prepend (GTK_MENU_SHELL (menu),
GTK_WIDGET (lang_menu_item));
}
if (priv->inline_checker != NULL)
{
/* Prepend suggestions */
_gspell_inline_checker_text_buffer_populate_popup (priv->inline_checker, menu);
}
}
static void
set_view (GspellTextView *gspell_view,
GtkTextView *gtk_view)
{
GspellTextViewPrivate *priv;
g_return_if_fail (GTK_IS_TEXT_VIEW (gtk_view));
priv = gspell_text_view_get_instance_private (gspell_view);
g_assert (priv->view == NULL);
g_assert (priv->inline_checker == NULL);
priv->view = gtk_view;
g_signal_connect_object (priv->view,
"notify::buffer",
G_CALLBACK (notify_buffer_cb),
gspell_view,
0);
/* G_CONNECT_AFTER, so when menu items are prepended, they have more
* chances to be the first in the menu.
*/
g_signal_connect_object (priv->view,
"populate-popup",
G_CALLBACK (populate_popup_cb),
gspell_view,
G_CONNECT_AFTER);
g_object_notify (G_OBJECT (gspell_view), "view");
}
static void
gspell_text_view_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
GspellTextView *gspell_view = GSPELL_TEXT_VIEW (object);
switch (prop_id)
{
case PROP_VIEW:
g_value_set_object (value, gspell_text_view_get_view (gspell_view));
break;
case PROP_INLINE_SPELL_CHECKING:
g_value_set_boolean (value, gspell_text_view_get_inline_spell_checking (gspell_view));
break;
case PROP_ENABLE_LANGUAGE_MENU:
g_value_set_boolean (value, gspell_text_view_get_enable_language_menu (gspell_view));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
gspell_text_view_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
GspellTextView *gspell_view = GSPELL_TEXT_VIEW (object);
switch (prop_id)
{
case PROP_VIEW:
set_view (gspell_view, g_value_get_object (value));
break;
case PROP_INLINE_SPELL_CHECKING:
gspell_text_view_set_inline_spell_checking (gspell_view, g_value_get_boolean (value));
break;
case PROP_ENABLE_LANGUAGE_MENU:
gspell_text_view_set_enable_language_menu (gspell_view, g_value_get_boolean (value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
gspell_text_view_dispose (GObject *object)
{
GspellTextViewPrivate *priv;
priv = gspell_text_view_get_instance_private (GSPELL_TEXT_VIEW (object));
if (priv->view != NULL && priv->inline_checker != NULL)
{
_gspell_inline_checker_text_buffer_detach_view (priv->inline_checker,
priv->view);
}
priv->view = NULL;
g_clear_object (&priv->inline_checker);
G_OBJECT_CLASS (gspell_text_view_parent_class)->dispose (object);
}
static void
gspell_text_view_class_init (GspellTextViewClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
object_class->get_property = gspell_text_view_get_property;
object_class->set_property = gspell_text_view_set_property;
object_class->dispose = gspell_text_view_dispose;
/**
* GspellTextView:view:
*
* The #GtkTextView.
*/
g_object_class_install_property (object_class,
PROP_VIEW,
g_param_spec_object ("view",
"View",
"",
GTK_TYPE_TEXT_VIEW,
G_PARAM_READWRITE |
G_PARAM_CONSTRUCT_ONLY |
G_PARAM_STATIC_STRINGS));
/**
* GspellTextView:inline-spell-checking:
*
* Whether the inline spell checking is enabled.
*/
g_object_class_install_property (object_class,
PROP_INLINE_SPELL_CHECKING,
g_param_spec_boolean ("inline-spell-checking",
"Inline Spell Checking",
"",
FALSE,
G_PARAM_READWRITE |
G_PARAM_STATIC_STRINGS));
/**
* GspellTextView:enable-language-menu:
*
* When the context menu is shown, whether to add a sub-menu to select
* the language for the spell checking.
*
* Since: 1.2
*/
g_object_class_install_property (object_class,
PROP_ENABLE_LANGUAGE_MENU,
g_param_spec_boolean ("enable-language-menu",
"Enable Language Menu",
"",
FALSE,
G_PARAM_READWRITE |
G_PARAM_STATIC_STRINGS));
}
static void
gspell_text_view_init (GspellTextView *gspell_view)
{
}
/**
* gspell_text_view_get_from_gtk_text_view:
* @gtk_view: a #GtkTextView.
*
* Returns the #GspellTextView of @gtk_view. The returned object is guaranteed
* to be the same for the lifetime of @gtk_view.
*
* Returns: (transfer none): the #GspellTextView of @gtk_view.
*/
GspellTextView *
gspell_text_view_get_from_gtk_text_view (GtkTextView *gtk_view)
{
GspellTextView *gspell_view;
g_return_val_if_fail (GTK_IS_TEXT_VIEW (gtk_view), NULL);
gspell_view = g_object_get_data (G_OBJECT (gtk_view), GSPELL_TEXT_VIEW_KEY);
if (gspell_view == NULL)
{
gspell_view = g_object_new (GSPELL_TYPE_TEXT_VIEW,
"view", gtk_view,
NULL);
g_object_set_data_full (G_OBJECT (gtk_view),
GSPELL_TEXT_VIEW_KEY,
gspell_view,
g_object_unref);
}
g_return_val_if_fail (GSPELL_IS_TEXT_VIEW (gspell_view), NULL);
return gspell_view;
}
/**
* gspell_text_view_basic_setup:
* @gspell_view: a #GspellTextView.
*
* This function is a convenience function that does the following:
* - Set a spell checker. The language chosen is the one returned by
* gspell_language_get_default().
* - Set the #GspellTextView:inline-spell-checking property to %TRUE.
* - Set the #GspellTextView:enable-language-menu property to %TRUE.
*
* Example:
* |[
* GtkTextView *gtk_view;
* GspellTextView *gspell_view;
*
* gspell_view = gspell_text_view_get_from_gtk_text_view (gtk_view);
* gspell_text_view_basic_setup (gspell_view);
* ]|
*
* This is equivalent to:
* |[
* GtkTextView *gtk_view;
* GspellTextView *gspell_view;
* GspellChecker *checker;
* GtkTextBuffer *gtk_buffer;
* GspellTextBuffer *gspell_buffer;
*
* checker = gspell_checker_new (NULL);
* gtk_buffer = gtk_text_view_get_buffer (gtk_view);
* gspell_buffer = gspell_text_buffer_get_from_gtk_text_buffer (gtk_buffer);
* gspell_text_buffer_set_spell_checker (gspell_buffer, checker);
* g_object_unref (checker);
*
* gspell_view = gspell_text_view_get_from_gtk_text_view (gtk_view);
* gspell_text_view_set_inline_spell_checking (gspell_view, TRUE);
* gspell_text_view_set_enable_language_menu (gspell_view, TRUE);
* ]|
*
* Since: 1.2
*/
void
gspell_text_view_basic_setup (GspellTextView *gspell_view)
{
GspellTextViewPrivate *priv;
GspellChecker *checker;
GtkTextBuffer *gtk_buffer;
GspellTextBuffer *gspell_buffer;
g_return_if_fail (GSPELL_IS_TEXT_VIEW (gspell_view));
priv = gspell_text_view_get_instance_private (gspell_view);
checker = gspell_checker_new (NULL);
gtk_buffer = gtk_text_view_get_buffer (priv->view);
gspell_buffer = gspell_text_buffer_get_from_gtk_text_buffer (gtk_buffer);
gspell_text_buffer_set_spell_checker (gspell_buffer, checker);
g_object_unref (checker);
gspell_text_view_set_inline_spell_checking (gspell_view, TRUE);
gspell_text_view_set_enable_language_menu (gspell_view, TRUE);
}
/**
* gspell_text_view_get_view:
* @gspell_view: a #GspellTextView.
*
* Returns: (transfer none): the #GtkTextView of @gspell_view.
*/
GtkTextView *
gspell_text_view_get_view (GspellTextView *gspell_view)
{
GspellTextViewPrivate *priv;
g_return_val_if_fail (GSPELL_IS_TEXT_VIEW (gspell_view), NULL);
priv = gspell_text_view_get_instance_private (gspell_view);
return priv->view;
}
/**
* gspell_text_view_get_inline_spell_checking:
* @gspell_view: a #GspellTextView.
*
* Returns: whether the inline spell checking is enabled.
*/
gboolean
gspell_text_view_get_inline_spell_checking (GspellTextView *gspell_view)
{
GspellTextViewPrivate *priv;
g_return_val_if_fail (GSPELL_IS_TEXT_VIEW (gspell_view), FALSE);
priv = gspell_text_view_get_instance_private (gspell_view);
return priv->inline_checker != NULL;
}
/**
* gspell_text_view_set_inline_spell_checking:
* @gspell_view: a #GspellTextView.
* @enable: the new state.
*
* Enables or disables the inline spell checking.
*/
void
gspell_text_view_set_inline_spell_checking (GspellTextView *gspell_view,
gboolean enable)
{
g_return_if_fail (GSPELL_IS_TEXT_VIEW (gspell_view));
enable = enable != FALSE;
if (enable == gspell_text_view_get_inline_spell_checking (gspell_view))
{
return;
}
if (enable)
{
create_inline_checker (gspell_view);
}
else
{
destroy_inline_checker (gspell_view);
}
g_object_notify (G_OBJECT (gspell_view), "inline-spell-checking");
}
/**
* gspell_text_view_get_enable_language_menu:
* @gspell_view: a #GspellTextView.
*
* Returns: whether the language context menu is enabled.
* Since: 1.2
*/
gboolean
gspell_text_view_get_enable_language_menu (GspellTextView *gspell_view)
{
GspellTextViewPrivate *priv;
g_return_val_if_fail (GSPELL_IS_TEXT_VIEW (gspell_view), FALSE);
priv = gspell_text_view_get_instance_private (gspell_view);
return priv->enable_language_menu;
}
/**
* gspell_text_view_set_enable_language_menu:
* @gspell_view: a #GspellTextView.
* @enable_language_menu: whether to enable the language context menu.
*
* Sets whether to enable the language context menu. If enabled, doing a right
* click on the #GtkTextView will show a sub-menu to choose the language for the
* spell checking. If another language is chosen, it changes the
* #GspellChecker:language property of the #GspellTextBuffer:spell-checker of
* the #GtkTextView:buffer of the #GspellTextView:view.
*
* Since: 1.2
*/
void
gspell_text_view_set_enable_language_menu (GspellTextView *gspell_view,
gboolean enable_language_menu)
{
GspellTextViewPrivate *priv;
g_return_if_fail (GSPELL_IS_TEXT_VIEW (gspell_view));
priv = gspell_text_view_get_instance_private (gspell_view);
enable_language_menu = enable_language_menu != FALSE;
if (priv->enable_language_menu != enable_language_menu)
{
priv->enable_language_menu = enable_language_menu;
g_object_notify (G_OBJECT (gspell_view), "enable-language-menu");
}
}
/* ex:set ts=8 noet: */