/*
* gedit-replace-dialog.c
* This file is part of gedit
*
* Copyright (C) 2005 Paolo Maggi
* Copyright (C) 2013 Sébastien Wilmet
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 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 .
*/
#include "config.h"
#include "gedit-replace-dialog.h"
#include
#include
#include
#include "gedit-history-entry.h"
#include "gedit-document.h"
#define GEDIT_SEARCH_CONTEXT_KEY "gedit-search-context-key"
struct _GeditReplaceDialog
{
GtkDialog parent_instance;
GtkWidget *grid;
GtkWidget *search_label;
GtkWidget *search_entry;
GtkWidget *search_text_entry;
GtkWidget *replace_label;
GtkWidget *replace_entry;
GtkWidget *replace_text_entry;
GtkWidget *match_case_checkbutton;
GtkWidget *entire_word_checkbutton;
GtkWidget *regex_checkbutton;
GtkWidget *backwards_checkbutton;
GtkWidget *wrap_around_checkbutton;
GtkWidget *close_button;
GeditDocument *active_document;
guint idle_update_sensitivity_id;
};
G_DEFINE_TYPE (GeditReplaceDialog, gedit_replace_dialog, GTK_TYPE_DIALOG)
static GtkSourceSearchContext *
get_search_context (GeditReplaceDialog *dialog,
GeditDocument *doc)
{
GtkSourceSearchContext *search_context;
if (doc == NULL)
{
return NULL;
}
search_context = gedit_document_get_search_context (doc);
if (search_context != NULL &&
g_object_get_data (G_OBJECT (search_context), GEDIT_SEARCH_CONTEXT_KEY) == dialog)
{
return search_context;
}
return NULL;
}
/* The search settings between the dialog's widgets (checkbuttons and the text
* entry) and the SearchSettings object are not bound. Instead, this function is
* called to set the search settings from the widgets to the SearchSettings
* object.
*
* The reason: the search and replace dialog is not an incremental search. You
* have to press the buttons to have an effect. The SearchContext is created
* only when a button is pressed, not before. If the SearchContext was created
* directly when the dialog window is shown, or when the document tab is
* switched, there would be a problem. When we switch betweeen tabs to find on
* which tab(s) we want to do the search, we may have older searches (still
* highlighted) that we don't want to lose, and we don't want the new search to
* appear on each tab that we open. Only when we press a button. So when the
* SearchContext is not already created, this is not an incremental search. Once
* the SearchContext is created, it's better to be consistent, and therefore we
* don't want the incremental search: we have to always press a button to
* execute the search.
*
* Likewise, each created SearchContext (from the GeditReplaceDialog) contains a
* different SearchSettings. When set_search_settings() is called for one
* document tab (and thus one SearchSettings), it doesn't have an effect on the
* other tabs. But the dialog widgets don't change.
*/
static void
set_search_settings (GeditReplaceDialog *dialog)
{
GtkSourceSearchContext *search_context;
GtkSourceSearchSettings *search_settings;
gboolean case_sensitive;
gboolean at_word_boundaries;
gboolean regex_enabled;
gboolean wrap_around;
const gchar *search_text;
search_context = get_search_context (dialog, dialog->active_document);
if (search_context == NULL)
{
return;
}
search_settings = gtk_source_search_context_get_settings (search_context);
case_sensitive = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (dialog->match_case_checkbutton));
gtk_source_search_settings_set_case_sensitive (search_settings, case_sensitive);
at_word_boundaries = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (dialog->entire_word_checkbutton));
gtk_source_search_settings_set_at_word_boundaries (search_settings, at_word_boundaries);
regex_enabled = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (dialog->regex_checkbutton));
gtk_source_search_settings_set_regex_enabled (search_settings, regex_enabled);
wrap_around = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (dialog->wrap_around_checkbutton));
gtk_source_search_settings_set_wrap_around (search_settings, wrap_around);
search_text = gtk_entry_get_text (GTK_ENTRY (dialog->search_text_entry));
if (regex_enabled)
{
gtk_source_search_settings_set_search_text (search_settings, search_text);
}
else
{
gchar *unescaped_search_text = gtk_source_utils_unescape_search_text (search_text);
gtk_source_search_settings_set_search_text (search_settings, unescaped_search_text);
g_free (unescaped_search_text);
}
}
static GeditWindow *
get_gedit_window (GeditReplaceDialog *dialog)
{
GtkWindow *transient_for = gtk_window_get_transient_for (GTK_WINDOW (dialog));
return transient_for != NULL ? GEDIT_WINDOW (transient_for) : NULL;
}
static GeditDocument *
get_active_document (GeditReplaceDialog *dialog)
{
GeditWindow *window = get_gedit_window (dialog);
return window != NULL ? gedit_window_get_active_document (window) : NULL;
}
void
gedit_replace_dialog_present_with_time (GeditReplaceDialog *dialog,
guint32 timestamp)
{
g_return_if_fail (GEDIT_REPLACE_DIALOG (dialog));
gtk_window_present_with_time (GTK_WINDOW (dialog), timestamp);
gtk_widget_grab_focus (dialog->search_text_entry);
}
static gboolean
gedit_replace_dialog_delete_event (GtkWidget *widget,
GdkEventAny *event)
{
/* prevent destruction */
return TRUE;
}
static void
set_error (GtkEntry *entry,
const gchar *error_msg)
{
if (error_msg == NULL || error_msg[0] == '\0')
{
gtk_entry_set_icon_from_gicon (entry, GTK_ENTRY_ICON_SECONDARY, NULL);
gtk_entry_set_icon_tooltip_text (entry, GTK_ENTRY_ICON_SECONDARY, NULL);
}
else
{
GIcon *icon = g_themed_icon_new_with_default_fallbacks ("dialog-error-symbolic");
gtk_entry_set_icon_from_gicon (entry, GTK_ENTRY_ICON_SECONDARY, icon);
gtk_entry_set_icon_tooltip_text (entry, GTK_ENTRY_ICON_SECONDARY, error_msg);
g_object_unref (icon);
}
}
static void
set_search_error (GeditReplaceDialog *dialog,
const gchar *error_msg)
{
set_error (GTK_ENTRY (dialog->search_text_entry), error_msg);
}
void
gedit_replace_dialog_set_replace_error (GeditReplaceDialog *dialog,
const gchar *error_msg)
{
set_error (GTK_ENTRY (dialog->replace_text_entry), error_msg);
}
static gboolean
has_search_error (GeditReplaceDialog *dialog)
{
GIcon *icon;
icon = gtk_entry_get_icon_gicon (GTK_ENTRY (dialog->search_text_entry),
GTK_ENTRY_ICON_SECONDARY);
return icon != NULL;
}
static gboolean
has_replace_error (GeditReplaceDialog *dialog)
{
GIcon *icon;
icon = gtk_entry_get_icon_gicon (GTK_ENTRY (dialog->replace_text_entry),
GTK_ENTRY_ICON_SECONDARY);
return icon != NULL;
}
static void
update_regex_error (GeditReplaceDialog *dialog)
{
GtkSourceSearchContext *search_context;
GError *regex_error;
set_search_error (dialog, NULL);
search_context = get_search_context (dialog, dialog->active_document);
if (search_context == NULL)
{
return;
}
regex_error = gtk_source_search_context_get_regex_error (search_context);
if (regex_error != NULL)
{
set_search_error (dialog, regex_error->message);
g_error_free (regex_error);
}
}
static gboolean
update_replace_response_sensitivity_cb (GeditReplaceDialog *dialog)
{
GtkSourceSearchContext *search_context;
GtkTextIter start;
GtkTextIter end;
gint pos;
if (has_replace_error (dialog))
{
gtk_dialog_set_response_sensitive (GTK_DIALOG (dialog),
GEDIT_REPLACE_DIALOG_REPLACE_RESPONSE,
FALSE);
dialog->idle_update_sensitivity_id = 0;
return G_SOURCE_REMOVE;
}
search_context = get_search_context (dialog, dialog->active_document);
if (search_context == NULL)
{
dialog->idle_update_sensitivity_id = 0;
return G_SOURCE_REMOVE;
}
gtk_text_buffer_get_selection_bounds (GTK_TEXT_BUFFER (dialog->active_document),
&start,
&end);
pos = gtk_source_search_context_get_occurrence_position (search_context,
&start,
&end);
if (pos < 0)
{
return G_SOURCE_CONTINUE;
}
gtk_dialog_set_response_sensitive (GTK_DIALOG (dialog),
GEDIT_REPLACE_DIALOG_REPLACE_RESPONSE,
pos > 0);
dialog->idle_update_sensitivity_id = 0;
return G_SOURCE_REMOVE;
}
static void
install_idle_update_sensitivity (GeditReplaceDialog *dialog)
{
if (dialog->idle_update_sensitivity_id != 0)
{
return;
}
dialog->idle_update_sensitivity_id =
g_idle_add ((GSourceFunc)update_replace_response_sensitivity_cb,
dialog);
}
static void
mark_set_cb (GtkTextBuffer *buffer,
GtkTextIter *location,
GtkTextMark *mark,
GeditReplaceDialog *dialog)
{
GtkTextMark *insert;
GtkTextMark *selection_bound;
insert = gtk_text_buffer_get_insert (buffer);
selection_bound = gtk_text_buffer_get_selection_bound (buffer);
if (mark == insert || mark == selection_bound)
{
install_idle_update_sensitivity (dialog);
}
}
static void
update_responses_sensitivity (GeditReplaceDialog *dialog)
{
const gchar *search_text;
gboolean sensitive = TRUE;
install_idle_update_sensitivity (dialog);
search_text = gtk_entry_get_text (GTK_ENTRY (dialog->search_text_entry));
if (search_text[0] == '\0')
{
gtk_dialog_set_response_sensitive (GTK_DIALOG (dialog),
GEDIT_REPLACE_DIALOG_FIND_RESPONSE,
FALSE);
gtk_dialog_set_response_sensitive (GTK_DIALOG (dialog),
GEDIT_REPLACE_DIALOG_REPLACE_ALL_RESPONSE,
FALSE);
return;
}
sensitive = !has_search_error (dialog);
gtk_dialog_set_response_sensitive (GTK_DIALOG (dialog),
GEDIT_REPLACE_DIALOG_FIND_RESPONSE,
sensitive);
if (has_replace_error (dialog))
{
sensitive = FALSE;
}
gtk_dialog_set_response_sensitive (GTK_DIALOG (dialog),
GEDIT_REPLACE_DIALOG_REPLACE_ALL_RESPONSE,
sensitive);
}
static void
regex_error_notify_cb (GeditReplaceDialog *dialog)
{
update_regex_error (dialog);
update_responses_sensitivity (dialog);
}
static void
disconnect_document (GeditReplaceDialog *dialog)
{
GtkSourceSearchContext *search_context;
if (dialog->active_document == NULL)
{
return;
}
search_context = get_search_context (dialog, dialog->active_document);
if (search_context != NULL)
{
g_signal_handlers_disconnect_by_func (search_context,
regex_error_notify_cb,
dialog);
}
g_signal_handlers_disconnect_by_func (dialog->active_document,
mark_set_cb,
dialog);
g_clear_object (&dialog->active_document);
}
static void
connect_active_document (GeditReplaceDialog *dialog)
{
GeditDocument *doc;
GtkSourceSearchContext *search_context;
disconnect_document (dialog);
doc = get_active_document (dialog);
if (doc == NULL)
{
return;
}
dialog->active_document = g_object_ref (doc);
search_context = get_search_context (dialog, doc);
if (search_context == NULL)
{
GtkSourceSearchSettings *settings = gtk_source_search_settings_new ();
search_context = gtk_source_search_context_new (GTK_SOURCE_BUFFER (doc),
settings);
/* Mark the search context that it comes from the search and
* replace dialog. Search contexts can be created also from the
* GeditViewFrame.
*/
g_object_set_data (G_OBJECT (search_context),
GEDIT_SEARCH_CONTEXT_KEY,
dialog);
gedit_document_set_search_context (doc, search_context);
g_object_unref (settings);
g_object_unref (search_context);
}
g_signal_connect_object (search_context,
"notify::regex-error",
G_CALLBACK (regex_error_notify_cb),
dialog,
G_CONNECT_SWAPPED);
g_signal_connect_object (doc,
"mark-set",
G_CALLBACK (mark_set_cb),
dialog,
0);
update_regex_error (dialog);
update_responses_sensitivity (dialog);
}
static void
response_cb (GtkDialog *dialog,
gint response_id)
{
GeditReplaceDialog *dlg = GEDIT_REPLACE_DIALOG (dialog);
const gchar *str;
switch (response_id)
{
case GEDIT_REPLACE_DIALOG_REPLACE_RESPONSE:
case GEDIT_REPLACE_DIALOG_REPLACE_ALL_RESPONSE:
str = gtk_entry_get_text (GTK_ENTRY (dlg->replace_text_entry));
if (*str != '\0')
{
gedit_history_entry_prepend_text
(GEDIT_HISTORY_ENTRY (dlg->replace_entry),
str);
}
/* fall through, so that we also save the find entry */
case GEDIT_REPLACE_DIALOG_FIND_RESPONSE:
str = gtk_entry_get_text (GTK_ENTRY (dlg->search_text_entry));
if (*str != '\0')
{
gedit_history_entry_prepend_text
(GEDIT_HISTORY_ENTRY (dlg->search_entry),
str);
}
}
switch (response_id)
{
case GEDIT_REPLACE_DIALOG_REPLACE_RESPONSE:
case GEDIT_REPLACE_DIALOG_REPLACE_ALL_RESPONSE:
case GEDIT_REPLACE_DIALOG_FIND_RESPONSE:
connect_active_document (GEDIT_REPLACE_DIALOG (dialog));
set_search_settings (GEDIT_REPLACE_DIALOG (dialog));
}
}
static void
gedit_replace_dialog_dispose (GObject *object)
{
GeditReplaceDialog *dialog = GEDIT_REPLACE_DIALOG (object);
g_clear_object (&dialog->active_document);
if (dialog->idle_update_sensitivity_id != 0)
{
g_source_remove (dialog->idle_update_sensitivity_id);
dialog->idle_update_sensitivity_id = 0;
}
G_OBJECT_CLASS (gedit_replace_dialog_parent_class)->dispose (object);
}
static void
gedit_replace_dialog_class_init (GeditReplaceDialogClass *klass)
{
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
gobject_class->dispose = gedit_replace_dialog_dispose;
widget_class->delete_event = gedit_replace_dialog_delete_event;
/* Bind class to template */
gtk_widget_class_set_template_from_resource (widget_class,
"/org/gnome/gedit/ui/gedit-replace-dialog.ui");
gtk_widget_class_bind_template_child (widget_class, GeditReplaceDialog, grid);
gtk_widget_class_bind_template_child (widget_class, GeditReplaceDialog, search_label);
gtk_widget_class_bind_template_child (widget_class, GeditReplaceDialog, replace_label);
gtk_widget_class_bind_template_child (widget_class, GeditReplaceDialog, match_case_checkbutton);
gtk_widget_class_bind_template_child (widget_class, GeditReplaceDialog, entire_word_checkbutton);
gtk_widget_class_bind_template_child (widget_class, GeditReplaceDialog, regex_checkbutton);
gtk_widget_class_bind_template_child (widget_class, GeditReplaceDialog, backwards_checkbutton);
gtk_widget_class_bind_template_child (widget_class, GeditReplaceDialog, wrap_around_checkbutton);
gtk_widget_class_bind_template_child (widget_class, GeditReplaceDialog, close_button);
}
static void
search_text_entry_changed (GtkEditable *editable,
GeditReplaceDialog *dialog)
{
set_search_error (dialog, NULL);
update_responses_sensitivity (dialog);
}
static void
replace_text_entry_changed (GtkEditable *editable,
GeditReplaceDialog *dialog)
{
gedit_replace_dialog_set_replace_error (dialog, NULL);
update_responses_sensitivity (dialog);
}
static void
regex_checkbutton_toggled (GtkToggleButton *checkbutton,
GeditReplaceDialog *dialog)
{
if (!gtk_toggle_button_get_active (checkbutton))
{
/* Remove the regex error state so the user can search again */
set_search_error (dialog, NULL);
update_responses_sensitivity (dialog);
}
}
/* TODO: move in gedit-document.c and share it with gedit-view-frame */
static gboolean
get_selected_text (GtkTextBuffer *doc,
gchar **selected_text,
gint *len)
{
GtkTextIter start, end;
g_return_val_if_fail (selected_text != NULL, FALSE);
g_return_val_if_fail (*selected_text == NULL, FALSE);
if (!gtk_text_buffer_get_selection_bounds (doc, &start, &end))
{
if (len != NULL)
{
len = 0;
}
return FALSE;
}
*selected_text = gtk_text_buffer_get_slice (doc, &start, &end, TRUE);
if (len != NULL)
{
*len = g_utf8_strlen (*selected_text, -1);
}
return TRUE;
}
static void
show_cb (GeditReplaceDialog *dialog)
{
GeditWindow *window;
GeditDocument *doc;
gboolean selection_exists;
gchar *selection = NULL;
gint selection_length;
window = get_gedit_window (dialog);
if (window == NULL)
{
return;
}
doc = get_active_document (dialog);
if (doc == NULL)
{
return;
}
selection_exists = get_selected_text (GTK_TEXT_BUFFER (doc),
&selection,
&selection_length);
if (selection_exists && selection != NULL && selection_length < 80)
{
gboolean regex_enabled;
gchar *escaped_selection;
regex_enabled = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (dialog->regex_checkbutton));
if (regex_enabled)
{
escaped_selection = g_regex_escape_string (selection, -1);
}
else
{
escaped_selection = gtk_source_utils_escape_search_text (selection);
}
gtk_entry_set_text (GTK_ENTRY (dialog->search_text_entry),
escaped_selection);
g_free (escaped_selection);
}
g_free (selection);
}
static void
hide_cb (GeditReplaceDialog *dialog)
{
disconnect_document (dialog);
}
static void
gedit_replace_dialog_init (GeditReplaceDialog *dlg)
{
gtk_widget_init_template (GTK_WIDGET (dlg));
dlg->search_entry = gedit_history_entry_new ("search-for-entry", TRUE);
gtk_widget_set_size_request (dlg->search_entry, 300, -1);
gtk_widget_set_hexpand (GTK_WIDGET (dlg->search_entry), TRUE);
dlg->search_text_entry = gedit_history_entry_get_entry (GEDIT_HISTORY_ENTRY (dlg->search_entry));
gtk_entry_set_activates_default (GTK_ENTRY (dlg->search_text_entry), TRUE);
gtk_grid_attach_next_to (GTK_GRID (dlg->grid),
dlg->search_entry,
dlg->search_label,
GTK_POS_RIGHT, 1, 1);
gtk_widget_show_all (dlg->search_entry);
dlg->replace_entry = gedit_history_entry_new ("replace-with-entry", TRUE);
gtk_widget_set_hexpand (GTK_WIDGET (dlg->replace_entry), TRUE);
dlg->replace_text_entry = gedit_history_entry_get_entry (GEDIT_HISTORY_ENTRY (dlg->replace_entry));
gtk_entry_set_placeholder_text (GTK_ENTRY (dlg->replace_text_entry), _("Nothing"));
gtk_entry_set_activates_default (GTK_ENTRY (dlg->replace_text_entry), TRUE);
gtk_grid_attach_next_to (GTK_GRID (dlg->grid),
dlg->replace_entry,
dlg->replace_label,
GTK_POS_RIGHT, 1, 1);
gtk_widget_show_all (dlg->replace_entry);
gtk_label_set_mnemonic_widget (GTK_LABEL (dlg->search_label),
dlg->search_entry);
gtk_label_set_mnemonic_widget (GTK_LABEL (dlg->replace_label),
dlg->replace_entry);
gtk_dialog_set_default_response (GTK_DIALOG (dlg),
GEDIT_REPLACE_DIALOG_FIND_RESPONSE);
/* insensitive by default */
gtk_dialog_set_response_sensitive (GTK_DIALOG (dlg),
GEDIT_REPLACE_DIALOG_FIND_RESPONSE,
FALSE);
gtk_dialog_set_response_sensitive (GTK_DIALOG (dlg),
GEDIT_REPLACE_DIALOG_REPLACE_RESPONSE,
FALSE);
gtk_dialog_set_response_sensitive (GTK_DIALOG (dlg),
GEDIT_REPLACE_DIALOG_REPLACE_ALL_RESPONSE,
FALSE);
g_signal_connect (dlg->search_text_entry,
"changed",
G_CALLBACK (search_text_entry_changed),
dlg);
g_signal_connect (dlg->replace_text_entry,
"changed",
G_CALLBACK (replace_text_entry_changed),
dlg);
g_signal_connect (dlg->regex_checkbutton,
"toggled",
G_CALLBACK (regex_checkbutton_toggled),
dlg);
g_signal_connect (dlg,
"show",
G_CALLBACK (show_cb),
NULL);
g_signal_connect (dlg,
"hide",
G_CALLBACK (hide_cb),
NULL);
/* We connect here to make sure this handler runs before the others so
* that the search context is created.
*/
g_signal_connect (dlg,
"response",
G_CALLBACK (response_cb),
NULL);
}
GtkWidget *
gedit_replace_dialog_new (GeditWindow *window)
{
GeditReplaceDialog *dialog;
gboolean use_header;
g_return_val_if_fail (GEDIT_IS_WINDOW (window), NULL);
dialog = g_object_new (GEDIT_TYPE_REPLACE_DIALOG,
"transient-for", window,
"destroy-with-parent", TRUE,
"use-header-bar", FALSE,
NULL);
/* We want the Find/Replace/ReplaceAll buttons at the bottom,
* so we turn off the automatic header bar, but we check the
* setting and if a header bar should be used, we create it
* manually and use it for the close button.
*/
g_object_get (gtk_settings_get_default (),
"gtk-dialogs-use-header", &use_header,
NULL);
if (use_header)
{
GtkWidget *header_bar;
header_bar = gtk_header_bar_new ();
gtk_header_bar_set_title (GTK_HEADER_BAR (header_bar), _("Find and Replace"));
gtk_header_bar_set_show_close_button (GTK_HEADER_BAR (header_bar), TRUE);
gtk_widget_show (header_bar);
gtk_window_set_titlebar (GTK_WINDOW (dialog), header_bar);
}
else
{
gtk_widget_set_no_show_all (dialog->close_button, FALSE);
gtk_widget_show (dialog->close_button);
}
return GTK_WIDGET (dialog);
}
const gchar *
gedit_replace_dialog_get_replace_text (GeditReplaceDialog *dialog)
{
g_return_val_if_fail (GEDIT_IS_REPLACE_DIALOG (dialog), NULL);
return gtk_entry_get_text (GTK_ENTRY (dialog->replace_text_entry));
}
gboolean
gedit_replace_dialog_get_backwards (GeditReplaceDialog *dialog)
{
g_return_val_if_fail (GEDIT_IS_REPLACE_DIALOG (dialog), FALSE);
return gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (dialog->backwards_checkbutton));
}
/* This function returns the original search text. The search text from the
* search settings has been unescaped, and the escape function is not
* reciprocal. So to avoid bugs, we have to deal with the original search text.
*/
const gchar *
gedit_replace_dialog_get_search_text (GeditReplaceDialog *dialog)
{
g_return_val_if_fail (GEDIT_IS_REPLACE_DIALOG (dialog), NULL);
return gtk_entry_get_text (GTK_ENTRY (dialog->search_text_entry));
}
/* ex:set ts=8 noet: */