/* * Copyright © 2015 Christian Persch * Copyright © 2005 Paolo Maggi * Copyright © 2010 Red Hat (Red Hat author: Behdad Esfahbod) * * 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 3 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 #include #include #include "terminal-pcre2.hh" #include "terminal-search-popover.hh" #include "terminal-intl.hh" #include "terminal-window.hh" #include "terminal-app.hh" #include "terminal-libgsystem.hh" typedef struct _TerminalSearchPopoverPrivate TerminalSearchPopoverPrivate; struct _TerminalSearchPopover { GtkWindow parent_instance; }; struct _TerminalSearchPopoverClass { GtkWindowClass parent_class; /* Signals */ void (* search) (TerminalSearchPopover *popover, gboolean backward); }; struct _TerminalSearchPopoverPrivate { GtkWidget *search_entry; GtkWidget *search_prev_button; GtkWidget *search_next_button; GtkWidget *reveal_button; GtkWidget *close_button; GtkWidget *revealer; GtkWidget *match_case_checkbutton; GtkWidget *entire_word_checkbutton; GtkWidget *regex_checkbutton; GtkWidget *wrap_around_checkbutton; gboolean search_text_changed; /* Cached regex */ gboolean regex_caseless; char *regex_pattern; VteRegex *regex; }; enum { PROP_0, PROP_REGEX, PROP_WRAP_AROUND, LAST_PROP }; enum { SEARCH, LAST_SIGNAL }; static guint signals[LAST_SIGNAL]; static GParamSpec *pspecs[LAST_PROP]; static GtkListStore *history_store; G_DEFINE_TYPE_WITH_PRIVATE (TerminalSearchPopover, terminal_search_popover, GTK_TYPE_WINDOW) #define PRIV(obj) ((TerminalSearchPopoverPrivate *) terminal_search_popover_get_instance_private ((TerminalSearchPopover *)(obj))) /* history */ #define HISTORY_MIN_ITEM_LEN (3) #define HISTORY_LENGTH (10) static gboolean history_enabled (void) { gboolean enabled; /* not quite an exact setting for this, but close enough… */ g_object_get (gtk_settings_get_default (), "gtk-recent-files-enabled", &enabled, nullptr); if (!enabled) return FALSE; if (history_store == nullptr) { history_store = gtk_list_store_new (1, G_TYPE_STRING); g_object_set_data_full (G_OBJECT (terminal_app_get ()), "search-history-store", history_store, (GDestroyNotify) g_object_unref); } return TRUE; } static gboolean history_remove_item (const char *text) { GtkTreeModel *model = GTK_TREE_MODEL (history_store); GtkTreeIter iter; if (!gtk_tree_model_get_iter_first (model, &iter)) return FALSE; do { gs_free gchar *item_text; gtk_tree_model_get (model, &iter, 0, &item_text, -1); if (item_text != nullptr && strcmp (item_text, text) == 0) { gtk_list_store_remove (history_store, &iter); return TRUE; } } while (gtk_tree_model_iter_next (model, &iter)); return FALSE; } static void history_clamp (int max) { GtkTreePath *path; GtkTreeIter iter; /* -1 because TreePath counts from 0 */ path = gtk_tree_path_new_from_indices (max - 1, -1); if (gtk_tree_model_get_iter (GTK_TREE_MODEL (history_store), &iter, path)) while (1) if (!gtk_list_store_remove (history_store, &iter)) break; gtk_tree_path_free (path); } static void history_insert_item (const char *text) { GtkTreeIter iter; if (!history_enabled () || text == nullptr) return; if (g_utf8_strlen (text, -1) <= HISTORY_MIN_ITEM_LEN) return; /* remove the text from the store if it was already * present. If it wasn't, clamp to max history - 1 * before inserting the new row, otherwise appending * would not work */ if (!history_remove_item (text)) history_clamp (HISTORY_LENGTH - 1); gtk_list_store_insert_with_values (history_store, &iter, 0, 0, text, -1); } /* helper functions */ static void update_sensitivity (TerminalSearchPopover *popover) { TerminalSearchPopoverPrivate *priv = PRIV (popover); gboolean can_search; can_search = priv->regex != nullptr; gtk_widget_set_sensitive (priv->search_prev_button, can_search); gtk_widget_set_sensitive (priv->search_next_button, can_search); } static void perform_search (TerminalSearchPopover *popover, gboolean backward) { TerminalSearchPopoverPrivate *priv = PRIV (popover); if (priv->regex == nullptr) return; /* Add to search history */ if (priv->search_text_changed) { const char *search_text; search_text = gtk_entry_get_text (GTK_ENTRY (priv->search_entry)); history_insert_item (search_text); priv->search_text_changed = FALSE; } g_signal_emit (popover, signals[SEARCH], 0, backward); } static void previous_match_cb (GtkWidget *widget, TerminalSearchPopover *popover) { perform_search (popover, TRUE); } static void next_match_cb (GtkWidget *widget, TerminalSearchPopover *popover) { perform_search (popover, FALSE); } static void close_clicked_cb (GtkWidget *widget, GtkWidget *popover) { gtk_widget_hide (popover); } static void search_button_clicked_cb (GtkWidget *button, TerminalSearchPopover *popover) { TerminalSearchPopoverPrivate *priv = PRIV (popover); perform_search (popover, button == priv->search_prev_button); } static gboolean key_press_cb (GtkWidget *popover, GdkEventKey *event, gpointer user_data G_GNUC_UNUSED) { if (event->keyval == GDK_KEY_Escape) { gtk_widget_hide (popover); return TRUE; } return FALSE; } static void update_regex (TerminalSearchPopover *popover) { TerminalSearchPopoverPrivate *priv = PRIV (popover); const char *search_text; gboolean caseless; gs_free char *pattern; gs_free_error GError *error = nullptr; search_text = gtk_entry_get_text (GTK_ENTRY (priv->search_entry)); caseless = !gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (priv->match_case_checkbutton)); if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (priv->regex_checkbutton))) { pattern = g_strdup (search_text); } else { pattern = g_regex_escape_string (search_text, -1); } if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (priv->entire_word_checkbutton))) { char *new_pattern; new_pattern = g_strdup_printf ("\\b%s\\b", pattern); g_free (pattern); pattern = new_pattern; } if (priv->regex_caseless == caseless && g_strcmp0 (priv->regex_pattern, pattern) == 0) return; if (priv->regex) { vte_regex_unref (priv->regex); } g_clear_pointer (&priv->regex_pattern, g_free); /* FIXME: if comping the regex fails, show the error message somewhere */ if (search_text[0] != '\0') { guint32 compile_flags; compile_flags = PCRE2_UTF | PCRE2_NO_UTF_CHECK | PCRE2_UCP | PCRE2_MULTILINE; if (caseless) compile_flags |= PCRE2_CASELESS; priv->regex = vte_regex_new_for_search (pattern, -1, compile_flags, &error); if (priv->regex != nullptr && (!vte_regex_jit (priv->regex, PCRE2_JIT_COMPLETE, nullptr) || !vte_regex_jit (priv->regex, PCRE2_JIT_PARTIAL_SOFT, nullptr))) { } if (priv->regex != nullptr) gs_transfer_out_value (&priv->regex_pattern, &pattern); } else { priv->regex = nullptr; } priv->regex_caseless = caseless; update_sensitivity (popover); g_object_notify_by_pspec (G_OBJECT (popover), pspecs[PROP_REGEX]); } static void search_text_changed_cb (GtkToggleButton *button, TerminalSearchPopover *popover) { TerminalSearchPopoverPrivate *priv = PRIV (popover); update_regex (popover); priv->search_text_changed = TRUE; } static void search_parameters_changed_cb (GtkToggleButton *button, TerminalSearchPopover *popover) { update_regex (popover); } static void wrap_around_toggled_cb (GtkToggleButton *button, TerminalSearchPopover *popover) { g_object_notify_by_pspec (G_OBJECT (popover), pspecs[PROP_WRAP_AROUND]); } /* public functions */ /* Class implementation */ static void terminal_search_popover_grab_focus (GtkWidget *widget) { TerminalSearchPopover *popover = TERMINAL_SEARCH_POPOVER (widget); TerminalSearchPopoverPrivate *priv = PRIV (popover); gtk_widget_grab_focus (priv->search_entry); } static void terminal_search_popover_init (TerminalSearchPopover *popover) { TerminalSearchPopoverPrivate *priv = PRIV (popover); GtkWidget *widget = GTK_WIDGET (popover); priv->regex_pattern = 0; priv->regex_caseless = TRUE; gtk_widget_init_template (widget); /* Make the search entry reasonably wide */ gtk_widget_set_size_request (priv->search_entry, 300, -1); /* Add entry completion with history */ #if 0 g_object_set (G_OBJECT (priv->search_entry), "model", history_store, "entry-text-column", 0, nullptr); #endif if (history_enabled ()) { gs_unref_object GtkEntryCompletion *completion; completion = gtk_entry_completion_new (); gtk_entry_completion_set_model (completion, GTK_TREE_MODEL (history_store)); gtk_entry_completion_set_text_column (completion, 0); gtk_entry_completion_set_minimum_key_length (completion, HISTORY_MIN_ITEM_LEN); gtk_entry_completion_set_popup_completion (completion, FALSE); gtk_entry_completion_set_inline_completion (completion, TRUE); gtk_entry_set_completion (GTK_ENTRY (priv->search_entry), completion); } #if 0 gtk_popover_set_default_widget (GTK_POPOVER (popover), priv->search_prev_button); #else GtkWindow *window = GTK_WINDOW (popover); gtk_window_set_default (window, priv->search_prev_button); #endif g_signal_connect (priv->search_entry, "previous-match", G_CALLBACK (previous_match_cb), popover); g_signal_connect (priv->search_entry, "next-match", G_CALLBACK (next_match_cb), popover); g_signal_connect (priv->search_prev_button, "clicked", G_CALLBACK (search_button_clicked_cb), popover); g_signal_connect (priv->search_next_button, "clicked", G_CALLBACK (search_button_clicked_cb), popover); g_signal_connect (priv->close_button, "clicked", G_CALLBACK (close_clicked_cb), popover); g_object_bind_property (priv->reveal_button, "active", priv->revealer, "reveal-child", G_BINDING_DEFAULT); update_sensitivity (popover); g_signal_connect (priv->search_entry, "search-changed", G_CALLBACK (search_text_changed_cb), popover); g_signal_connect (priv->match_case_checkbutton, "toggled", G_CALLBACK (search_parameters_changed_cb), popover); g_signal_connect (priv->entire_word_checkbutton, "toggled", G_CALLBACK (search_parameters_changed_cb), popover); g_signal_connect (priv->regex_checkbutton, "toggled", G_CALLBACK (search_parameters_changed_cb), popover); g_signal_connect (priv->wrap_around_checkbutton, "toggled", G_CALLBACK (wrap_around_toggled_cb), popover); g_signal_connect (popover, "key-press-event", G_CALLBACK (key_press_cb), nullptr); if (terminal_app_get_dialog_use_headerbar (terminal_app_get ())) { GtkWidget *headerbar; headerbar = (GtkWidget*)g_object_new (GTK_TYPE_HEADER_BAR, "title", gtk_window_get_title (window), "has-subtitle", FALSE, "show-close-button", TRUE, "visible", TRUE, nullptr); gtk_style_context_add_class (gtk_widget_get_style_context (headerbar), "default-decoration"); gtk_window_set_titlebar (window, headerbar); } } static void terminal_search_popover_finalize (GObject *object) { TerminalSearchPopover *popover = TERMINAL_SEARCH_POPOVER (object); TerminalSearchPopoverPrivate *priv = PRIV (popover); if (priv->regex) { vte_regex_unref (priv->regex); } g_free (priv->regex_pattern); G_OBJECT_CLASS (terminal_search_popover_parent_class)->finalize (object); } static void terminal_search_popover_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { TerminalSearchPopover *popover = TERMINAL_SEARCH_POPOVER (object); switch (prop_id) { case PROP_REGEX: g_value_set_boxed (value, terminal_search_popover_get_regex (popover)); break; case PROP_WRAP_AROUND: g_value_set_boolean (value, terminal_search_popover_get_wrap_around (popover)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void terminal_search_popover_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { switch (prop_id) { case PROP_REGEX: case PROP_WRAP_AROUND: /* not writable */ break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void terminal_search_popover_class_init (TerminalSearchPopoverClass *klass) { GObjectClass *gobject_class = G_OBJECT_CLASS (klass); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); gobject_class->finalize = terminal_search_popover_finalize; gobject_class->get_property = terminal_search_popover_get_property; gobject_class->set_property = terminal_search_popover_set_property; widget_class->grab_focus = terminal_search_popover_grab_focus; signals[SEARCH] = g_signal_new (I_("search"), G_OBJECT_CLASS_TYPE (gobject_class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (TerminalSearchPopoverClass, search), nullptr, nullptr, g_cclosure_marshal_VOID__BOOLEAN, G_TYPE_NONE, 1, G_TYPE_BOOLEAN); pspecs[PROP_REGEX] = g_param_spec_boxed ("regex", nullptr, nullptr, VTE_TYPE_REGEX, GParamFlags(G_PARAM_READABLE | G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB)); pspecs[PROP_WRAP_AROUND] = g_param_spec_boolean ("wrap-around", nullptr, nullptr, FALSE, GParamFlags(G_PARAM_READABLE | G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB)); g_object_class_install_properties (gobject_class, G_N_ELEMENTS (pspecs), pspecs); gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/terminal/ui/search-popover.ui"); gtk_widget_class_bind_template_child_private (widget_class, TerminalSearchPopover, search_entry); gtk_widget_class_bind_template_child_private (widget_class, TerminalSearchPopover, search_prev_button); gtk_widget_class_bind_template_child_private (widget_class, TerminalSearchPopover, search_next_button); gtk_widget_class_bind_template_child_private (widget_class, TerminalSearchPopover, reveal_button); gtk_widget_class_bind_template_child_private (widget_class, TerminalSearchPopover, close_button); gtk_widget_class_bind_template_child_private (widget_class, TerminalSearchPopover, revealer); gtk_widget_class_bind_template_child_private (widget_class, TerminalSearchPopover, match_case_checkbutton); gtk_widget_class_bind_template_child_private (widget_class, TerminalSearchPopover, entire_word_checkbutton); gtk_widget_class_bind_template_child_private (widget_class, TerminalSearchPopover, regex_checkbutton); gtk_widget_class_bind_template_child_private (widget_class, TerminalSearchPopover, wrap_around_checkbutton); } /* public API */ /** * terminal_search_popover_new: * * Returns: a new #TerminalSearchPopover */ TerminalSearchPopover * terminal_search_popover_new (GtkWidget *relative_to_widget) { return reinterpret_cast (g_object_new (TERMINAL_TYPE_SEARCH_POPOVER, #if 0 "relative-to", relative_to_widget, #else "transient-for", gtk_widget_get_toplevel (relative_to_widget), #endif nullptr)); } /** * terminal_search_popover_get_regex: * @popover: a #TerminalSearchPopover * * Returns: (transfer none): the search regex, or %nullptr */ VteRegex * terminal_search_popover_get_regex (TerminalSearchPopover *popover) { g_return_val_if_fail (TERMINAL_IS_SEARCH_POPOVER (popover), nullptr); return PRIV (popover)->regex; } /** * terminal_search_popover_get_wrap_around: * @popover: a #TerminalSearchPopover * * Returns: (transfer none): whether search should wrap around */ gboolean terminal_search_popover_get_wrap_around (TerminalSearchPopover *popover) { g_return_val_if_fail (TERMINAL_IS_SEARCH_POPOVER (popover), FALSE); return gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (PRIV (popover)->wrap_around_checkbutton)); }