/*
* This file is part of gspell, a spell-checking library.
*
* Copyright 2002 - Paolo Maggi
* Copyright 2015 - 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-checker-dialog.h"
#include <glib/gi18n-lib.h>
#include "gspell-checker.h"
/**
* SECTION:checker-dialog
* @Short_description: Spell checker dialog
* @Title: GspellCheckerDialog
* @See_also: #GspellNavigator
*
* #GspellCheckerDialog is a #GtkDialog to spell check a document one word
* at a time. It uses a #GspellNavigator.
*/
typedef struct _GspellCheckerDialogPrivate GspellCheckerDialogPrivate;
struct _GspellCheckerDialogPrivate
{
GspellNavigator *navigator;
GspellChecker *checker;
gchar *misspelled_word;
GtkLabel *misspelled_word_label;
GtkEntry *word_entry;
GtkWidget *check_word_button;
GtkWidget *ignore_button;
GtkWidget *ignore_all_button;
GtkWidget *change_button;
GtkWidget *change_all_button;
GtkWidget *add_word_button;
GtkTreeView *suggestions_view;
guint initialized : 1;
};
enum
{
PROP_0,
PROP_SPELL_NAVIGATOR,
};
enum
{
COLUMN_SUGGESTION,
N_COLUMNS
};
G_DEFINE_TYPE_WITH_PRIVATE (GspellCheckerDialog, gspell_checker_dialog, GTK_TYPE_DIALOG)
static void
set_spell_checker (GspellCheckerDialog *dialog,
GspellChecker *checker)
{
GspellCheckerDialogPrivate *priv;
priv = gspell_checker_dialog_get_instance_private (dialog);
if (g_set_object (&priv->checker, checker))
{
GtkHeaderBar *header_bar;
const GspellLanguage *lang;
header_bar = GTK_HEADER_BAR (gtk_dialog_get_header_bar (GTK_DIALOG (dialog)));
lang = gspell_checker_get_language (checker);
gtk_header_bar_set_subtitle (header_bar,
gspell_language_get_name (lang));
}
}
static void
set_navigator (GspellCheckerDialog *dialog,
GspellNavigator *navigator)
{
GspellCheckerDialogPrivate *priv;
priv = gspell_checker_dialog_get_instance_private (dialog);
g_return_if_fail (priv->navigator == NULL);
priv->navigator = g_object_ref_sink (navigator);
g_object_notify (G_OBJECT (dialog), "spell-navigator");
}
static void
clear_suggestions (GspellCheckerDialog *dialog)
{
GspellCheckerDialogPrivate *priv;
GtkListStore *store;
priv = gspell_checker_dialog_get_instance_private (dialog);
store = GTK_LIST_STORE (gtk_tree_view_get_model (priv->suggestions_view));
gtk_list_store_clear (store);
gtk_tree_view_columns_autosize (priv->suggestions_view);
}
static void
set_suggestions (GspellCheckerDialog *dialog,
GSList *suggestions)
{
GspellCheckerDialogPrivate *priv;
GtkListStore *store;
GtkTreeIter iter;
GtkTreeSelection *selection;
const gchar *first_suggestion;
GSList *l;
priv = gspell_checker_dialog_get_instance_private (dialog);
clear_suggestions (dialog);
store = GTK_LIST_STORE (gtk_tree_view_get_model (priv->suggestions_view));
if (suggestions == NULL)
{
gtk_list_store_append (store, &iter);
gtk_list_store_set (store, &iter,
/* Translators: Displayed in the "Check Spelling"
* dialog if there are no suggestions for the current
* misspelled word.
*/
COLUMN_SUGGESTION, _("(no suggested words)"),
-1);
gtk_entry_set_text (priv->word_entry, "");
gtk_widget_set_sensitive (GTK_WIDGET (priv->suggestions_view), FALSE);
return;
}
gtk_widget_set_sensitive (GTK_WIDGET (priv->suggestions_view), TRUE);
first_suggestion = suggestions->data;
gtk_entry_set_text (priv->word_entry, first_suggestion);
for (l = suggestions; l != NULL; l = l->next)
{
const gchar *suggestion = l->data;
gtk_list_store_append (store, &iter);
gtk_list_store_set (store, &iter,
COLUMN_SUGGESTION, suggestion,
-1);
}
selection = gtk_tree_view_get_selection (priv->suggestions_view);
gtk_tree_model_get_iter_first (GTK_TREE_MODEL (store), &iter);
gtk_tree_selection_select_iter (selection, &iter);
}
static void
set_misspelled_word (GspellCheckerDialog *dialog,
const gchar *word)
{
GspellCheckerDialogPrivate *priv;
gchar *label;
GSList *suggestions;
g_assert (word != NULL);
priv = gspell_checker_dialog_get_instance_private (dialog);
g_return_if_fail (!gspell_checker_check_word (priv->checker, word, -1, NULL));
g_free (priv->misspelled_word);
priv->misspelled_word = g_strdup (word);
label = g_strdup_printf("<b>%s</b>", word);
gtk_label_set_markup (priv->misspelled_word_label, label);
g_free (label);
suggestions = gspell_checker_get_suggestions (priv->checker, priv->misspelled_word, -1);
set_suggestions (dialog, suggestions);
g_slist_free_full (suggestions, g_free);
}
static void
set_completed (GspellCheckerDialog *dialog)
{
GspellCheckerDialogPrivate *priv;
priv = gspell_checker_dialog_get_instance_private (dialog);
clear_suggestions (dialog);
gtk_entry_set_text (priv->word_entry, "");
gtk_widget_set_sensitive (GTK_WIDGET (priv->word_entry), FALSE);
gtk_widget_set_sensitive (priv->check_word_button, FALSE);
gtk_widget_set_sensitive (priv->ignore_button, FALSE);
gtk_widget_set_sensitive (priv->ignore_all_button, FALSE);
gtk_widget_set_sensitive (priv->change_button, FALSE);
gtk_widget_set_sensitive (priv->change_all_button, FALSE);
gtk_widget_set_sensitive (priv->add_word_button, FALSE);
gtk_widget_set_sensitive (GTK_WIDGET (priv->suggestions_view), FALSE);
}
static void
show_error (GspellCheckerDialog *dialog,
GError *error)
{
GspellCheckerDialogPrivate *priv;
gchar *label;
priv = gspell_checker_dialog_get_instance_private (dialog);
label = g_strdup_printf ("<b>%s</b> %s", _("Error:"), error->message);
gtk_label_set_markup (priv->misspelled_word_label, label);
g_free (label);
set_completed (dialog);
}
static void
goto_next (GspellCheckerDialog *dialog)
{
GspellCheckerDialogPrivate *priv;
gchar *word = NULL;
GspellChecker *checker = NULL;
GError *error = NULL;
gboolean found;
priv = gspell_checker_dialog_get_instance_private (dialog);
found = gspell_navigator_goto_next (priv->navigator, &word, &checker, &error);
if (error != NULL)
{
show_error (dialog, error);
g_clear_error (&error);
}
else if (found)
{
set_spell_checker (dialog, checker);
set_misspelled_word (dialog, word);
}
else
{
gchar *label;
if (priv->initialized)
{
label = g_strdup_printf ("<b>%s</b>", _("Completed spell checking"));
}
else
{
label = g_strdup_printf ("<b>%s</b>", _("No misspelled words"));
}
gtk_label_set_markup (priv->misspelled_word_label, label);
g_free (label);
set_completed (dialog);
}
priv->initialized = TRUE;
g_free (word);
g_clear_object (&checker);
}
static void
gspell_checker_dialog_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
GspellCheckerDialog *dialog = GSPELL_CHECKER_DIALOG (object);
switch (prop_id)
{
case PROP_SPELL_NAVIGATOR:
g_value_set_object (value, gspell_checker_dialog_get_spell_navigator (dialog));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
gspell_checker_dialog_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
GspellCheckerDialog *dialog = GSPELL_CHECKER_DIALOG (object);
switch (prop_id)
{
case PROP_SPELL_NAVIGATOR:
set_navigator (dialog, g_value_get_object (value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
gspell_checker_dialog_dispose (GObject *object)
{
GspellCheckerDialogPrivate *priv;
priv = gspell_checker_dialog_get_instance_private (GSPELL_CHECKER_DIALOG (object));
g_clear_object (&priv->navigator);
g_clear_object (&priv->checker);
G_OBJECT_CLASS (gspell_checker_dialog_parent_class)->dispose (object);
}
static void
gspell_checker_dialog_finalize (GObject *object)
{
GspellCheckerDialogPrivate *priv;
priv = gspell_checker_dialog_get_instance_private (GSPELL_CHECKER_DIALOG (object));
g_free (priv->misspelled_word);
G_OBJECT_CLASS (gspell_checker_dialog_parent_class)->finalize (object);
}
static void
gspell_checker_dialog_show (GtkWidget *widget)
{
GspellCheckerDialog *dialog = GSPELL_CHECKER_DIALOG (widget);
GspellCheckerDialogPrivate *priv;
priv = gspell_checker_dialog_get_instance_private (dialog);
/* Chain-up */
if (GTK_WIDGET_CLASS (gspell_checker_dialog_parent_class)->show != NULL)
{
GTK_WIDGET_CLASS (gspell_checker_dialog_parent_class)->show (widget);
}
/* A typical implementation of a SpellNavigator is to select the
* misspelled word when goto_next() is called. Showing the dialog makes
* a focus change, which can unselect the buffer selection (e.g. in a
* GtkTextBuffer). So that's why goto_next() is called after the
* chain-up.
*/
if (!priv->initialized)
{
goto_next (dialog);
}
}
static void
gspell_checker_dialog_class_init (GspellCheckerDialogClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
object_class->get_property = gspell_checker_dialog_get_property;
object_class->set_property = gspell_checker_dialog_set_property;
object_class->dispose = gspell_checker_dialog_dispose;
object_class->finalize = gspell_checker_dialog_finalize;
widget_class->show = gspell_checker_dialog_show;
/**
* GspellCheckerDialog:spell-navigator:
*
* The #GspellNavigator to use.
*/
g_object_class_install_property (object_class,
PROP_SPELL_NAVIGATOR,
g_param_spec_object ("spell-navigator",
"Spell Navigator",
"",
GSPELL_TYPE_NAVIGATOR,
G_PARAM_READWRITE |
G_PARAM_CONSTRUCT_ONLY |
G_PARAM_STATIC_STRINGS));
/* Bind class to template */
gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/gspell/checker-dialog.ui");
gtk_widget_class_bind_template_child_private (widget_class, GspellCheckerDialog, misspelled_word_label);
gtk_widget_class_bind_template_child_private (widget_class, GspellCheckerDialog, word_entry);
gtk_widget_class_bind_template_child_private (widget_class, GspellCheckerDialog, check_word_button);
gtk_widget_class_bind_template_child_private (widget_class, GspellCheckerDialog, ignore_button);
gtk_widget_class_bind_template_child_private (widget_class, GspellCheckerDialog, ignore_all_button);
gtk_widget_class_bind_template_child_private (widget_class, GspellCheckerDialog, change_button);
gtk_widget_class_bind_template_child_private (widget_class, GspellCheckerDialog, change_all_button);
gtk_widget_class_bind_template_child_private (widget_class, GspellCheckerDialog, add_word_button);
gtk_widget_class_bind_template_child_private (widget_class, GspellCheckerDialog, suggestions_view);
}
static void
word_entry_changed_handler (GtkEntry *word_entry,
GspellCheckerDialog *dialog)
{
GspellCheckerDialogPrivate *priv;
gboolean sensitive;
priv = gspell_checker_dialog_get_instance_private (dialog);
sensitive = gtk_entry_get_text_length (word_entry) > 0;
gtk_widget_set_sensitive (priv->check_word_button, sensitive);
gtk_widget_set_sensitive (priv->change_button, sensitive);
gtk_widget_set_sensitive (priv->change_all_button, sensitive);
}
static void
suggestions_selection_changed_handler (GtkTreeSelection *selection,
GspellCheckerDialog *dialog)
{
GspellCheckerDialogPrivate *priv;
GtkTreeModel *model;
GtkTreeIter iter;
gchar *text;
priv = gspell_checker_dialog_get_instance_private (dialog);
if (!gtk_tree_selection_get_selected (selection, &model, &iter))
{
return;
}
gtk_tree_model_get (model, &iter,
COLUMN_SUGGESTION, &text,
-1);
gtk_entry_set_text (priv->word_entry, text);
g_free (text);
}
static void
check_word_button_clicked_handler (GtkButton *button,
GspellCheckerDialog *dialog)
{
GspellCheckerDialogPrivate *priv;
const gchar *word;
gboolean correctly_spelled;
GError *error = NULL;
priv = gspell_checker_dialog_get_instance_private (dialog);
g_return_if_fail (gtk_entry_get_text_length (priv->word_entry) > 0);
word = gtk_entry_get_text (priv->word_entry);
correctly_spelled = gspell_checker_check_word (priv->checker, word, -1, &error);
if (error != NULL)
{
show_error (dialog, error);
g_error_free (error);
return;
}
if (correctly_spelled)
{
GtkListStore *store;
GtkTreeIter iter;
clear_suggestions (dialog);
store = GTK_LIST_STORE (gtk_tree_view_get_model (priv->suggestions_view));
gtk_list_store_append (store, &iter);
gtk_list_store_set (store, &iter,
/* Translators: Displayed in the "Check
* Spelling" dialog if the current word
* isn't misspelled.
*/
COLUMN_SUGGESTION, _("(correct spelling)"),
-1);
gtk_widget_set_sensitive (GTK_WIDGET (priv->suggestions_view), FALSE);
}
else
{
GSList *suggestions;
suggestions = gspell_checker_get_suggestions (priv->checker, word, -1);
set_suggestions (dialog, suggestions);
g_slist_free_full (suggestions, g_free);
}
}
static void
add_word_button_clicked_handler (GtkButton *button,
GspellCheckerDialog *dialog)
{
GspellCheckerDialogPrivate *priv;
priv = gspell_checker_dialog_get_instance_private (dialog);
g_return_if_fail (priv->misspelled_word != NULL);
gspell_checker_add_word_to_personal (priv->checker, priv->misspelled_word, -1);
goto_next (dialog);
}
static void
ignore_button_clicked_handler (GtkButton *button,
GspellCheckerDialog *dialog)
{
goto_next (dialog);
}
static void
ignore_all_button_clicked_handler (GtkButton *button,
GspellCheckerDialog *dialog)
{
GspellCheckerDialogPrivate *priv;
priv = gspell_checker_dialog_get_instance_private (dialog);
g_return_if_fail (priv->misspelled_word != NULL);
gspell_checker_add_word_to_session (priv->checker, priv->misspelled_word, -1);
goto_next (dialog);
}
static void
change_button_clicked_handler (GtkButton *button,
GspellCheckerDialog *dialog)
{
GspellCheckerDialogPrivate *priv;
const gchar *entry_text;
gchar *change_to;
priv = gspell_checker_dialog_get_instance_private (dialog);
g_return_if_fail (priv->misspelled_word != NULL);
entry_text = gtk_entry_get_text (priv->word_entry);
g_return_if_fail (entry_text != NULL);
g_return_if_fail (entry_text[0] != '\0');
change_to = g_strdup (entry_text);
gspell_checker_set_correction (priv->checker,
priv->misspelled_word, -1,
change_to, -1);
gspell_navigator_change (priv->navigator, priv->misspelled_word, change_to);
g_free (change_to);
goto_next (dialog);
}
/* double click on one of the suggestions is like clicking on "change" */
static void
suggestions_row_activated_handler (GtkTreeView *view,
GtkTreePath *path,
GtkTreeViewColumn *column,
GspellCheckerDialog *dialog)
{
GspellCheckerDialogPrivate *priv;
priv = gspell_checker_dialog_get_instance_private (dialog);
change_button_clicked_handler (GTK_BUTTON (priv->change_button), dialog);
}
static void
change_all_button_clicked_handler (GtkButton *button,
GspellCheckerDialog *dialog)
{
GspellCheckerDialogPrivate *priv;
const gchar *entry_text;
gchar *change_to;
priv = gspell_checker_dialog_get_instance_private (dialog);
g_return_if_fail (priv->misspelled_word != NULL);
entry_text = gtk_entry_get_text (priv->word_entry);
g_return_if_fail (entry_text != NULL);
g_return_if_fail (entry_text[0] != '\0');
change_to = g_strdup (entry_text);
gspell_checker_set_correction (priv->checker,
priv->misspelled_word, -1,
change_to, -1);
gspell_navigator_change_all (priv->navigator, priv->misspelled_word, change_to);
g_free (change_to);
goto_next (dialog);
}
static void
gspell_checker_dialog_init (GspellCheckerDialog *dialog)
{
GspellCheckerDialogPrivate *priv;
GtkListStore *store;
GtkTreeViewColumn *column;
GtkCellRenderer *cell;
GtkTreeSelection *selection;
priv = gspell_checker_dialog_get_instance_private (dialog);
gtk_widget_init_template (GTK_WIDGET (dialog));
/* Suggestion list */
store = gtk_list_store_new (N_COLUMNS, G_TYPE_STRING);
gtk_tree_view_set_model (priv->suggestions_view, GTK_TREE_MODEL (store));
g_object_unref (store);
/* Add the suggestions column */
cell = gtk_cell_renderer_text_new ();
column = gtk_tree_view_column_new_with_attributes (_("Suggestions"), cell,
"text", COLUMN_SUGGESTION,
NULL);
gtk_tree_view_append_column (priv->suggestions_view, column);
gtk_tree_view_set_search_column (priv->suggestions_view, COLUMN_SUGGESTION);
selection = gtk_tree_view_get_selection (priv->suggestions_view);
gtk_tree_selection_set_mode (selection, GTK_SELECTION_SINGLE);
/* Connect signals */
g_signal_connect (priv->word_entry,
"changed",
G_CALLBACK (word_entry_changed_handler),
dialog);
g_signal_connect_object (selection,
"changed",
G_CALLBACK (suggestions_selection_changed_handler),
dialog,
0);
g_signal_connect (priv->check_word_button,
"clicked",
G_CALLBACK (check_word_button_clicked_handler),
dialog);
g_signal_connect (priv->add_word_button,
"clicked",
G_CALLBACK (add_word_button_clicked_handler),
dialog);
g_signal_connect (priv->ignore_button,
"clicked",
G_CALLBACK (ignore_button_clicked_handler),
dialog);
g_signal_connect (priv->ignore_all_button,
"clicked",
G_CALLBACK (ignore_all_button_clicked_handler),
dialog);
g_signal_connect (priv->change_button,
"clicked",
G_CALLBACK (change_button_clicked_handler),
dialog);
g_signal_connect (priv->change_all_button,
"clicked",
G_CALLBACK (change_all_button_clicked_handler),
dialog);
g_signal_connect (priv->suggestions_view,
"row-activated",
G_CALLBACK (suggestions_row_activated_handler),
dialog);
gtk_widget_grab_default (priv->change_button);
}
/**
* gspell_checker_dialog_new:
* @parent: transient parent of the dialog.
* @navigator: the #GspellNavigator to use.
*
* Returns: a new #GspellCheckerDialog widget.
*/
GtkWidget *
gspell_checker_dialog_new (GtkWindow *parent,
GspellNavigator *navigator)
{
g_return_val_if_fail (GTK_IS_WINDOW (parent), NULL);
g_return_val_if_fail (GSPELL_IS_NAVIGATOR (navigator), NULL);
return g_object_new (GSPELL_TYPE_CHECKER_DIALOG,
"transient-for", parent,
"use-header-bar", TRUE,
"spell-navigator", navigator,
NULL);
}
/**
* gspell_checker_dialog_get_spell_navigator:
* @dialog: a #GspellCheckerDialog.
*
* Returns: (transfer none): the #GspellNavigator used.
*/
GspellNavigator *
gspell_checker_dialog_get_spell_navigator (GspellCheckerDialog *dialog)
{
GspellCheckerDialogPrivate *priv;
g_return_val_if_fail (GSPELL_IS_CHECKER_DIALOG (dialog), NULL);
priv = gspell_checker_dialog_get_instance_private (dialog);
return priv->navigator;
}
/* ex:set ts=8 noet: */