From 39ee1b7890e06779b47f0fc11925d12caa206c39 Mon Sep 17 00:00:00 2001 From: Matthew Barnes Date: Sun, 17 Jan 2010 17:47:08 -0500 Subject: Give all preview panes a search bar. Use Shift+Ctrl+F as the accelerator for consistency with the mailer. --- widgets/misc/Makefile.am | 6 + widgets/misc/e-preview-pane.c | 275 ++++++++ widgets/misc/e-preview-pane.h | 74 +++ widgets/misc/e-search-bar.c | 825 +++++++++++++++++++++++ widgets/misc/e-search-bar.h | 87 +++ widgets/misc/e-searching-tokenizer.c | 1195 ++++++++++++++++++++++++++++++++++ widgets/misc/e-searching-tokenizer.h | 92 +++ 7 files changed, 2554 insertions(+) create mode 100644 widgets/misc/e-preview-pane.c create mode 100644 widgets/misc/e-preview-pane.h create mode 100644 widgets/misc/e-search-bar.c create mode 100644 widgets/misc/e-search-bar.h create mode 100644 widgets/misc/e-searching-tokenizer.c create mode 100644 widgets/misc/e-searching-tokenizer.h (limited to 'widgets/misc') diff --git a/widgets/misc/Makefile.am b/widgets/misc/Makefile.am index 5853039fb0..44d8c9a577 100644 --- a/widgets/misc/Makefile.am +++ b/widgets/misc/Makefile.am @@ -58,7 +58,10 @@ widgetsinclude_HEADERS = \ e-popup-action.h \ e-popup-menu.h \ e-preferences-window.h \ + e-preview-pane.h \ e-printable.h \ + e-search-bar.h \ + e-searching-tokenizer.h \ e-selectable.h \ e-selection-model.h \ e-selection-model-array.h \ @@ -133,7 +136,10 @@ libemiscwidgets_la_SOURCES = \ e-popup-action.c \ e-popup-menu.c \ e-preferences-window.c \ + e-preview-pane.c \ e-printable.c \ + e-search-bar.c \ + e-searching-tokenizer.c \ e-selectable.c \ e-selection-model.c \ e-selection-model-array.c \ diff --git a/widgets/misc/e-preview-pane.c b/widgets/misc/e-preview-pane.c new file mode 100644 index 0000000000..240bfbd824 --- /dev/null +++ b/widgets/misc/e-preview-pane.c @@ -0,0 +1,275 @@ +/* + * e-preview-pane.c + * + * This program 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 License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#include "e-preview-pane.h" + +#include + +#define E_PREVIEW_PANE_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_PREVIEW_PANE, EPreviewPanePrivate)) + +struct _EPreviewPanePrivate { + GtkWidget *web_view; + GtkWidget *search_bar; +}; + +enum { + PROP_0, + PROP_SEARCH_BAR, + PROP_WEB_VIEW +}; + +enum { + SHOW_SEARCH_BAR, + LAST_SIGNAL +}; + +static gpointer parent_class; +static guint signals[LAST_SIGNAL]; + +static void +preview_pane_set_web_view (EPreviewPane *preview_pane, + EWebView *web_view) +{ + g_return_if_fail (E_IS_WEB_VIEW (web_view)); + g_return_if_fail (preview_pane->priv->web_view == NULL); + + preview_pane->priv->web_view = g_object_ref_sink (web_view); +} + +static void +preview_pane_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_WEB_VIEW: + preview_pane_set_web_view ( + E_PREVIEW_PANE (object), + g_value_get_object (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +preview_pane_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_SEARCH_BAR: + g_value_set_object ( + value, e_preview_pane_get_search_bar ( + E_PREVIEW_PANE (object))); + return; + + case PROP_WEB_VIEW: + g_value_set_object ( + value, e_preview_pane_get_web_view ( + E_PREVIEW_PANE (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +preview_pane_dispose (GObject *object) +{ + EPreviewPanePrivate *priv; + + priv = E_PREVIEW_PANE_GET_PRIVATE (object); + + if (priv->search_bar != NULL) { + g_object_unref (priv->search_bar); + priv->search_bar = NULL; + } + + if (priv->web_view != NULL) { + g_object_unref (priv->web_view); + priv->web_view = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (parent_class)->dispose (object); +} + +static void +preview_pane_constructed (GObject *object) +{ + EPreviewPanePrivate *priv; + GtkWidget *widget; + + priv = E_PREVIEW_PANE_GET_PRIVATE (object); + + widget = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_policy ( + GTK_SCROLLED_WINDOW (widget), + GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + gtk_scrolled_window_set_shadow_type ( + GTK_SCROLLED_WINDOW (widget), GTK_SHADOW_IN); + gtk_box_pack_start (GTK_BOX (object), widget, TRUE, TRUE, 0); + gtk_container_add (GTK_CONTAINER (widget), priv->web_view); + gtk_widget_show (widget); + + widget = e_search_bar_new (E_WEB_VIEW (priv->web_view)); + gtk_box_pack_start (GTK_BOX (object), widget, FALSE, FALSE, 0); + priv->search_bar = g_object_ref (widget); + gtk_widget_hide (widget); +} + +static void +preview_pane_show_search_bar (EPreviewPane *preview_pane) +{ + GtkWidget *search_bar; + + search_bar = preview_pane->priv->search_bar; + + if (!gtk_widget_get_visible (search_bar)) + gtk_widget_show (search_bar); +} + +static void +preview_pane_class_init (EPreviewPaneClass *class) +{ + GObjectClass *object_class; + GtkBindingSet *binding_set; + + parent_class = g_type_class_peek_parent (class); + g_type_class_add_private (class, sizeof (EPreviewPanePrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = preview_pane_set_property; + object_class->get_property = preview_pane_get_property; + object_class->dispose = preview_pane_dispose; + object_class->constructed = preview_pane_constructed; + + class->show_search_bar = preview_pane_show_search_bar; + + g_object_class_install_property ( + object_class, + PROP_SEARCH_BAR, + g_param_spec_object ( + "search-bar", + "Search Bar", + NULL, + E_TYPE_SEARCH_BAR, + G_PARAM_READABLE)); + + g_object_class_install_property ( + object_class, + PROP_WEB_VIEW, + g_param_spec_object ( + "web-view", + "Web View", + NULL, + E_TYPE_WEB_VIEW, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY)); + + signals[SHOW_SEARCH_BAR] = g_signal_new ( + "show-search-bar", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET (EPreviewPaneClass, show_search_bar), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + binding_set = gtk_binding_set_by_class (class); + + gtk_binding_entry_add_signal ( + binding_set, GDK_f, GDK_SHIFT_MASK | GDK_CONTROL_MASK, + "show-search-bar", 0); +} + +static void +preview_pane_init (EPreviewPane *preview_pane) +{ + preview_pane->priv = E_PREVIEW_PANE_GET_PRIVATE (preview_pane); + + gtk_box_set_spacing (GTK_BOX (preview_pane), 1); +} + +GType +e_preview_pane_get_type (void) +{ + static GType type = 0; + + if (G_UNLIKELY (type == 0)) { + static const GTypeInfo type_info = { + sizeof (EPreviewPaneClass), + (GBaseInitFunc) NULL, + (GBaseFinalizeFunc) NULL, + (GClassInitFunc) preview_pane_class_init, + (GClassFinalizeFunc) NULL, + NULL, /* class_data */ + sizeof (EPreviewPane), + 0, /* n_preallocs */ + (GInstanceInitFunc) preview_pane_init, + NULL /* value_table */ + }; + + type = g_type_register_static ( + GTK_TYPE_VBOX, "EPreviewPane", &type_info, 0); + } + + return type; +} + +GtkWidget * +e_preview_pane_new (EWebView *web_view) +{ + g_return_val_if_fail (E_IS_WEB_VIEW (web_view), NULL); + + return g_object_new ( + E_TYPE_PREVIEW_PANE, + "web-view", web_view, NULL); +} + +EWebView * +e_preview_pane_get_web_view (EPreviewPane *preview_pane) +{ + g_return_val_if_fail (E_IS_PREVIEW_PANE (preview_pane), NULL); + + return E_WEB_VIEW (preview_pane->priv->web_view); +} + +ESearchBar * +e_preview_pane_get_search_bar (EPreviewPane *preview_pane) +{ + g_return_val_if_fail (E_IS_PREVIEW_PANE (preview_pane), NULL); + + return E_SEARCH_BAR (preview_pane->priv->search_bar); +} + +void +e_preview_pane_show_search_bar (EPreviewPane *preview_pane) +{ + g_return_if_fail (E_IS_PREVIEW_PANE (preview_pane)); + + g_signal_emit (preview_pane, signals[SHOW_SEARCH_BAR], 0); +} diff --git a/widgets/misc/e-preview-pane.h b/widgets/misc/e-preview-pane.h new file mode 100644 index 0000000000..bd965ed307 --- /dev/null +++ b/widgets/misc/e-preview-pane.h @@ -0,0 +1,74 @@ +/* + * e-preview-pane.h + * + * This program 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 License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifndef E_PREVIEW_PANE_H +#define E_PREVIEW_PANE_H + +#include +#include +#include + +/* Standard GObject macros */ +#define E_TYPE_PREVIEW_PANE \ + (e_preview_pane_get_type ()) +#define E_PREVIEW_PANE(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_PREVIEW_PANE, EPreviewPane)) +#define E_PREVIEW_PANE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_PREVIEW_PANE, EPreviewPaneClass)) +#define E_IS_PREVIEW_PANE(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_PREVIEW_PANE)) +#define E_IS_PREVIEW_PANE_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_PREVIEW_PANE)) +#define E_PREVIEW_PANE_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_PREVIEW_PANE, EPreviewPaneClass)) + +G_BEGIN_DECLS + +typedef struct _EPreviewPane EPreviewPane; +typedef struct _EPreviewPaneClass EPreviewPaneClass; +typedef struct _EPreviewPanePrivate EPreviewPanePrivate; + +struct _EPreviewPane { + GtkVBox parent; + EPreviewPanePrivate *priv; +}; + +struct _EPreviewPaneClass { + GtkVBoxClass parent_class; + + /* Signals */ + void (*show_search_bar) (EPreviewPane *preview_pane); +}; + +GType e_preview_pane_get_type (void); +GtkWidget * e_preview_pane_new (EWebView *web_view); +EWebView * e_preview_pane_get_web_view (EPreviewPane *preview_pane); +ESearchBar * e_preview_pane_get_search_bar (EPreviewPane *preview_pane); +void e_preview_pane_show_search_bar (EPreviewPane *preview_pane); + +G_END_DECLS + +#endif /* E_PREVIEW_PANE_H */ diff --git a/widgets/misc/e-search-bar.c b/widgets/misc/e-search-bar.c new file mode 100644 index 0000000000..120f114bb8 --- /dev/null +++ b/widgets/misc/e-search-bar.c @@ -0,0 +1,825 @@ +/* + * e-search-bar.c + * + * This program 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 License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#include "e-search-bar.h" + +#include +#include +#include + +#include "e-util/e-binding.h" + +#define E_SEARCH_BAR_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_SEARCH_BAR, ESearchBarPrivate)) + +struct _ESearchBarPrivate { + EWebView *web_view; + GtkWidget *entry; + GtkWidget *case_sensitive_button; + GtkWidget *wrapped_next_box; + GtkWidget *wrapped_prev_box; + GtkWidget *matches_label; + + ESearchingTokenizer *tokenizer; + gchar *active_search; + + guint rerun_search : 1; +}; + +enum { + PROP_0, + PROP_ACTIVE_SEARCH, + PROP_CASE_SENSITIVE, + PROP_TEXT, + PROP_WEB_VIEW +}; + +enum { + CHANGED, + CLEAR, + LAST_SIGNAL +}; + +static gpointer parent_class; +static guint signals[LAST_SIGNAL]; + +static void +search_bar_update_matches (ESearchBar *search_bar) +{ + ESearchingTokenizer *tokenizer; + GtkWidget *matches_label; + gint matches; + gchar *text; + + tokenizer = e_search_bar_get_tokenizer (search_bar); + matches_label = search_bar->priv->matches_label; + + matches = e_searching_tokenizer_match_count (tokenizer); + text = g_strdup_printf (_("Matches: %d"), matches); + + gtk_label_set_text (GTK_LABEL (matches_label), text); + gtk_widget_show (matches_label); + + g_free (text); +} + +static void +search_bar_update_tokenizer (ESearchBar *search_bar) +{ + ESearchingTokenizer *tokenizer; + gboolean case_sensitive; + gchar *active_search; + + tokenizer = e_search_bar_get_tokenizer (search_bar); + case_sensitive = e_search_bar_get_case_sensitive (search_bar); + + if (GTK_WIDGET_VISIBLE (search_bar)) + active_search = search_bar->priv->active_search; + else + active_search = NULL; + + e_searching_tokenizer_set_primary_case_sensitivity ( + tokenizer, case_sensitive); + e_searching_tokenizer_set_primary_search_string ( + tokenizer, active_search); + + e_search_bar_changed (search_bar); +} + +static void +search_bar_find (ESearchBar *search_bar, + gboolean search_forward) +{ + EWebView *web_view; + GtkWidget *widget; + gboolean case_sensitive; + gboolean new_search; + gboolean wrapped = FALSE; + gchar *text; + + web_view = e_search_bar_get_web_view (search_bar); + case_sensitive = e_search_bar_get_case_sensitive (search_bar); + text = e_search_bar_get_text (search_bar); + + if (text == NULL || *text == '\0') { + e_search_bar_clear (search_bar); + g_free (text); + return; + } + + new_search = + (search_bar->priv->active_search == NULL) || + (g_strcmp0 (text, search_bar->priv->active_search) != 0); + + /* XXX On a new search, the HTMLEngine's search state gets + * destroyed when we redraw the message with highlighted + * matches (EMHTMLStream's write() method triggers this, + * but it's really GtkHtml's fault). That's why the first + * match isn't selected automatically. It also causes + * gtk_html_engine_search_next() to return FALSE, which we + * assume to mean the search wrapped. + * + * So to avoid mistakenly thinking the search wrapped when + * it hasn't, we have to trap the first button click after a + * search and re-run the search to recreate the HTMLEngine's + * search state, so that gtk_html_engine_search_next() will + * succeed. */ + if (new_search) { + g_free (search_bar->priv->active_search); + search_bar->priv->active_search = text; + search_bar->priv->rerun_search = TRUE; + search_bar_update_tokenizer (search_bar); + } else if (search_bar->priv->rerun_search) { + gtk_html_engine_search ( + GTK_HTML (web_view), + search_bar->priv->active_search, + case_sensitive, search_forward, FALSE); + search_bar->priv->rerun_search = FALSE; + g_free (text); + } else { + gtk_html_engine_search_set_forward ( + GTK_HTML (web_view), search_forward); + if (!gtk_html_engine_search_next (GTK_HTML (web_view))) + wrapped = TRUE; + g_free (text); + } + + if (new_search || wrapped) + gtk_html_engine_search ( + GTK_HTML (web_view), + search_bar->priv->active_search, + case_sensitive, search_forward, FALSE); + + g_object_notify (G_OBJECT (search_bar), "active-search"); + + /* Update wrapped label visibility. */ + + widget = search_bar->priv->wrapped_next_box; + + if (wrapped && search_forward) + gtk_widget_show (widget); + else + gtk_widget_hide (widget); + + widget = search_bar->priv->wrapped_prev_box; + + if (wrapped && !search_forward) + gtk_widget_show (widget); + else + gtk_widget_hide (widget); +} + +static void +search_bar_changed_cb (ESearchBar *search_bar) +{ + g_object_notify (G_OBJECT (search_bar), "text"); +} + +static void +search_bar_find_next_cb (ESearchBar *search_bar) +{ + search_bar_find (search_bar, TRUE); +} + +static void +search_bar_find_previous_cb (ESearchBar *search_bar) +{ + search_bar_find (search_bar, FALSE); +} + +static void +search_bar_icon_release_cb (ESearchBar *search_bar, + GtkEntryIconPosition icon_pos, + GdkEvent *event) +{ + g_return_if_fail (icon_pos == GTK_ENTRY_ICON_SECONDARY); + + e_search_bar_clear (search_bar); + gtk_widget_grab_focus (search_bar->priv->entry); +} + +static void +search_bar_toggled_cb (ESearchBar *search_bar) +{ + g_free (search_bar->priv->active_search); + search_bar->priv->active_search = NULL; + + g_object_notify (G_OBJECT (search_bar), "active-search"); + g_object_notify (G_OBJECT (search_bar), "case-sensitive"); +} + +static void +search_bar_set_web_view (ESearchBar *search_bar, + EWebView *web_view) +{ + GtkHTML *html; + ESearchingTokenizer *tokenizer; + + g_return_if_fail (search_bar->priv->web_view == NULL); + + search_bar->priv->web_view = g_object_ref (web_view); + + html = GTK_HTML (web_view); + tokenizer = e_search_bar_get_tokenizer (search_bar); + gtk_html_set_tokenizer (html, HTML_TOKENIZER (tokenizer)); +} + +static void +search_bar_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_CASE_SENSITIVE: + e_search_bar_set_case_sensitive ( + E_SEARCH_BAR (object), + g_value_get_boolean (value)); + return; + + case PROP_TEXT: + e_search_bar_set_text ( + E_SEARCH_BAR (object), + g_value_get_string (value)); + return; + + case PROP_WEB_VIEW: + search_bar_set_web_view ( + E_SEARCH_BAR (object), + g_value_get_object (value)); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +search_bar_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + switch (property_id) { + case PROP_ACTIVE_SEARCH: + g_value_set_boolean ( + value, e_search_bar_get_active_search ( + E_SEARCH_BAR (object))); + return; + + case PROP_CASE_SENSITIVE: + g_value_set_boolean ( + value, e_search_bar_get_case_sensitive ( + E_SEARCH_BAR (object))); + return; + + case PROP_TEXT: + g_value_take_string ( + value, e_search_bar_get_text ( + E_SEARCH_BAR (object))); + return; + + case PROP_WEB_VIEW: + g_value_set_object ( + value, e_search_bar_get_web_view ( + E_SEARCH_BAR (object))); + return; + } + + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); +} + +static void +search_bar_dispose (GObject *object) +{ + ESearchBarPrivate *priv; + + priv = E_SEARCH_BAR_GET_PRIVATE (object); + + if (priv->web_view != NULL) { + g_object_unref (priv->web_view); + priv->web_view = NULL; + } + + if (priv->entry != NULL) { + g_object_unref (priv->entry); + priv->entry = NULL; + } + + if (priv->case_sensitive_button != NULL) { + g_object_unref (priv->case_sensitive_button); + priv->case_sensitive_button = NULL; + } + + if (priv->wrapped_next_box != NULL) { + g_object_unref (priv->wrapped_next_box); + priv->wrapped_next_box = NULL; + } + + if (priv->wrapped_prev_box != NULL) { + g_object_unref (priv->wrapped_prev_box); + priv->wrapped_prev_box = NULL; + } + + if (priv->matches_label != NULL) { + g_object_unref (priv->matches_label); + priv->matches_label = NULL; + } + + if (priv->tokenizer != NULL) { + g_object_unref (priv->tokenizer); + priv->tokenizer = NULL; + } + + /* Chain up to parent's dispose() method. */ + G_OBJECT_CLASS (parent_class)->dispose (object); +} + +static void +search_bar_finalize (GObject *object) +{ + ESearchBarPrivate *priv; + + priv = E_SEARCH_BAR_GET_PRIVATE (object); + + g_free (priv->active_search); + + /* Chain up to parent's finalize() method. */ + G_OBJECT_CLASS (parent_class)->finalize (object); +} + +static void +search_bar_constructed (GObject *object) +{ + ESearchBarPrivate *priv; + + priv = E_SEARCH_BAR_GET_PRIVATE (object); + + e_mutual_binding_new ( + object, "case-sensitive", + priv->case_sensitive_button, "active"); +} + +static void +search_bar_show (GtkWidget *widget) +{ + ESearchBar *search_bar; + + search_bar = E_SEARCH_BAR (widget); + + /* Chain up to parent's show() method. */ + GTK_WIDGET_CLASS (parent_class)->show (widget); + + gtk_widget_grab_focus (search_bar->priv->entry); + + search_bar_update_tokenizer (search_bar); +} + +static void +search_bar_hide (GtkWidget *widget) +{ + ESearchBar *search_bar; + + search_bar = E_SEARCH_BAR (widget); + + /* Chain up to parent's hide() method. */ + GTK_WIDGET_CLASS (parent_class)->hide (widget); + + search_bar_update_tokenizer (search_bar); +} + +static gboolean +search_bar_key_press_event (GtkWidget *widget, + GdkEventKey *event) +{ + GtkWidgetClass *widget_class; + + if (event->keyval == GDK_Escape) { + gtk_widget_hide (widget); + return TRUE; + } + + /* Chain up to parent's key_press_event() method. */ + widget_class = GTK_WIDGET_CLASS (parent_class); + return widget_class->key_press_event (widget, event); +} + +static void +search_bar_clear (ESearchBar *search_bar) +{ + g_free (search_bar->priv->active_search); + search_bar->priv->active_search = NULL; + + gtk_entry_set_text (GTK_ENTRY (search_bar->priv->entry), ""); + + gtk_widget_hide (search_bar->priv->wrapped_next_box); + gtk_widget_hide (search_bar->priv->wrapped_prev_box); + gtk_widget_hide (search_bar->priv->matches_label); + + search_bar_update_tokenizer (search_bar); + + g_object_notify (G_OBJECT (search_bar), "active-search"); +} + +static void +search_bar_class_init (ESearchBarClass *class) +{ + GObjectClass *object_class; + GtkWidgetClass *widget_class; + + parent_class = g_type_class_peek_parent (class); + g_type_class_add_private (class, sizeof (ESearchBarPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->set_property = search_bar_set_property; + object_class->get_property = search_bar_get_property; + object_class->dispose = search_bar_dispose; + object_class->finalize = search_bar_finalize; + object_class->constructed = search_bar_constructed; + + widget_class = GTK_WIDGET_CLASS (class); + widget_class->show = search_bar_show; + widget_class->hide = search_bar_hide; + widget_class->key_press_event = search_bar_key_press_event; + + class->clear = search_bar_clear; + + g_object_class_install_property ( + object_class, + PROP_ACTIVE_SEARCH, + g_param_spec_boolean ( + "active-search", + "Active Search", + NULL, + FALSE, + G_PARAM_READABLE)); + + g_object_class_install_property ( + object_class, + PROP_CASE_SENSITIVE, + g_param_spec_boolean ( + "case-sensitive", + "Case Sensitive", + NULL, + FALSE, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_TEXT, + g_param_spec_string ( + "text", + "Search Text", + NULL, + NULL, + G_PARAM_READWRITE)); + + g_object_class_install_property ( + object_class, + PROP_WEB_VIEW, + g_param_spec_object ( + "web-view", + "Web View", + NULL, + E_TYPE_WEB_VIEW, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY)); + + signals[CHANGED] = g_signal_new ( + "changed", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET (ESearchBarClass, changed), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + signals[CLEAR] = g_signal_new ( + "clear", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, + G_STRUCT_OFFSET (ESearchBarClass, clear), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); +} + +static void +search_bar_init (ESearchBar *search_bar) +{ + GtkWidget *label; + GtkWidget *widget; + GtkWidget *container; + + search_bar->priv = E_SEARCH_BAR_GET_PRIVATE (search_bar); + search_bar->priv->tokenizer = e_searching_tokenizer_new (); + + g_signal_connect_swapped ( + search_bar->priv->tokenizer, "match", + G_CALLBACK (search_bar_update_matches), search_bar); + + gtk_box_set_spacing (GTK_BOX (search_bar), 12); + + container = GTK_WIDGET (search_bar); + + widget = gtk_hbox_new (FALSE, 1); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + gtk_widget_show (widget); + + container = widget; + + widget = gtk_button_new (); + gtk_button_set_image ( + GTK_BUTTON (widget), gtk_image_new_from_stock ( + GTK_STOCK_CLOSE, GTK_ICON_SIZE_MENU)); + gtk_button_set_relief (GTK_BUTTON (widget), GTK_RELIEF_NONE); + gtk_widget_set_tooltip_text (widget, _("Close the find bar")); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + gtk_widget_show (widget); + + g_signal_connect_swapped ( + widget, "clicked", + G_CALLBACK (gtk_widget_hide), search_bar); + + widget = gtk_label_new_with_mnemonic (_("Fin_d:")); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 3); + gtk_widget_show (widget); + + label = widget; + + widget = gtk_entry_new (); + gtk_entry_set_icon_from_stock ( + GTK_ENTRY (widget), GTK_ENTRY_ICON_SECONDARY, + GTK_STOCK_CLEAR); + gtk_entry_set_icon_tooltip_text ( + GTK_ENTRY (widget), GTK_ENTRY_ICON_SECONDARY, + _("Clear the search")); + gtk_label_set_mnemonic_widget (GTK_LABEL (label), widget); + gtk_widget_set_size_request (widget, 200, -1); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + search_bar->priv->entry = g_object_ref (widget); + gtk_widget_show (widget); + + e_binding_new ( + search_bar, "active-search", + widget, "secondary-icon-sensitive"); + + g_signal_connect_swapped ( + widget, "activate", + G_CALLBACK (search_bar_find_next_cb), search_bar); + + g_signal_connect_swapped ( + widget, "changed", + G_CALLBACK (search_bar_changed_cb), search_bar); + + g_signal_connect_swapped ( + widget, "icon-release", + G_CALLBACK (search_bar_icon_release_cb), search_bar); + + widget = gtk_button_new_with_mnemonic (_("_Previous")); + gtk_button_set_image ( + GTK_BUTTON (widget), gtk_image_new_from_stock ( + GTK_STOCK_GO_BACK, GTK_ICON_SIZE_MENU)); + gtk_button_set_relief (GTK_BUTTON (widget), GTK_RELIEF_NONE); + gtk_widget_set_tooltip_text ( + widget, _("Find the previous occurrence of the phrase")); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + gtk_widget_show (widget); + + e_binding_new (search_bar, "active-search", widget, "sensitive"); + + g_signal_connect_swapped ( + widget, "clicked", + G_CALLBACK (search_bar_find_previous_cb), search_bar); + + widget = gtk_button_new_with_mnemonic (_("_Next")); + gtk_button_set_image ( + GTK_BUTTON (widget), gtk_image_new_from_stock ( + GTK_STOCK_GO_FORWARD, GTK_ICON_SIZE_MENU)); + gtk_button_set_relief (GTK_BUTTON (widget), GTK_RELIEF_NONE); + gtk_widget_set_tooltip_text ( + widget, _("Find the next occurrence of the phrase")); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + gtk_widget_show (widget); + + e_binding_new (search_bar, "active-search", widget, "sensitive"); + + g_signal_connect_swapped ( + widget, "clicked", + G_CALLBACK (search_bar_find_next_cb), search_bar); + + widget = gtk_check_button_new_with_mnemonic (_("Mat_ch case")); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + search_bar->priv->case_sensitive_button = g_object_ref (widget); + gtk_widget_show (widget); + + g_signal_connect_swapped ( + widget, "toggled", + G_CALLBACK (search_bar_toggled_cb), search_bar); + + g_signal_connect_swapped ( + widget, "toggled", + G_CALLBACK (search_bar_find_next_cb), search_bar); + + container = GTK_WIDGET (search_bar); + + widget = gtk_hbox_new (FALSE, 6); + gtk_box_pack_start (GTK_BOX (container), widget, TRUE, TRUE, 0); + search_bar->priv->wrapped_next_box = g_object_ref (widget); + gtk_widget_hide (widget); + + container = widget; + + widget = gtk_image_new_from_icon_name ( + "wrapped", GTK_ICON_SIZE_MENU); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + gtk_widget_show (widget); + + widget = gtk_label_new ( + _("Reached bottom of page, continued from top")); + gtk_label_set_ellipsize ( + GTK_LABEL (widget), PANGO_ELLIPSIZE_END); + gtk_misc_set_alignment (GTK_MISC (widget), 0.0, 0.5); + gtk_box_pack_start (GTK_BOX (container), widget, TRUE, TRUE, 0); + gtk_widget_show (widget); + + container = GTK_WIDGET (search_bar); + + widget = gtk_hbox_new (FALSE, 6); + gtk_box_pack_start (GTK_BOX (container), widget, TRUE, TRUE, 0); + search_bar->priv->wrapped_prev_box = g_object_ref (widget); + gtk_widget_hide (widget); + + container = widget; + + widget = gtk_image_new_from_icon_name ( + "wrapped", GTK_ICON_SIZE_MENU); + gtk_box_pack_start (GTK_BOX (container), widget, FALSE, FALSE, 0); + gtk_widget_show (widget); + + widget = gtk_label_new ( + _("Reached top of page, continued from bottom")); + gtk_label_set_ellipsize ( + GTK_LABEL (widget), PANGO_ELLIPSIZE_END); + gtk_misc_set_alignment (GTK_MISC (widget), 0.0, 0.5); + gtk_box_pack_start (GTK_BOX (container), widget, TRUE, TRUE, 0); + gtk_widget_show (widget); + + container = GTK_WIDGET (search_bar); + + widget = gtk_label_new (NULL); + gtk_box_pack_end (GTK_BOX (container), widget, FALSE, FALSE, 12); + search_bar->priv->matches_label = g_object_ref (widget); + gtk_widget_show (widget); +} + +GType +e_search_bar_get_type (void) +{ + static GType type = 0; + + if (G_UNLIKELY (type == 0)) { + static const GTypeInfo type_info = { + sizeof (ESearchBarClass), + (GBaseInitFunc) NULL, + (GBaseFinalizeFunc) NULL, + (GClassInitFunc) search_bar_class_init, + (GClassFinalizeFunc) NULL, + NULL, /* class_data */ + sizeof (ESearchBar), + 0, /* n_preallocs */ + (GInstanceInitFunc) search_bar_init, + NULL /* value_table */ + }; + + type = g_type_register_static ( + GTK_TYPE_HBOX, "ESearchBar", &type_info, 0); + } + + return type; +} + +GtkWidget * +e_search_bar_new (EWebView *web_view) +{ + g_return_val_if_fail (E_IS_WEB_VIEW (web_view), NULL); + + return g_object_new ( + E_TYPE_SEARCH_BAR, "web-view", web_view, NULL); +} + +void +e_search_bar_clear (ESearchBar *search_bar) +{ + g_return_if_fail (E_IS_SEARCH_BAR (search_bar)); + + g_signal_emit (search_bar, signals[CLEAR], 0); +} + +void +e_search_bar_changed (ESearchBar *search_bar) +{ + g_return_if_fail (E_IS_SEARCH_BAR (search_bar)); + + g_signal_emit (search_bar, signals[CHANGED], 0); +} + +EWebView * +e_search_bar_get_web_view (ESearchBar *search_bar) +{ + g_return_val_if_fail (E_IS_SEARCH_BAR (search_bar), NULL); + + return search_bar->priv->web_view; +} + +ESearchingTokenizer * +e_search_bar_get_tokenizer (ESearchBar *search_bar) +{ + g_return_val_if_fail (E_IS_SEARCH_BAR (search_bar), NULL); + + return search_bar->priv->tokenizer; +} + +gboolean +e_search_bar_get_active_search (ESearchBar *search_bar) +{ + g_return_val_if_fail (E_IS_SEARCH_BAR (search_bar), FALSE); + + return (search_bar->priv->active_search != NULL); +} + +gboolean +e_search_bar_get_case_sensitive (ESearchBar *search_bar) +{ + GtkToggleButton *button; + + g_return_val_if_fail (E_IS_SEARCH_BAR (search_bar), FALSE); + + button = GTK_TOGGLE_BUTTON (search_bar->priv->case_sensitive_button); + + return gtk_toggle_button_get_active (button); +} + +void +e_search_bar_set_case_sensitive (ESearchBar *search_bar, + gboolean case_sensitive) +{ + GtkToggleButton *button; + + g_return_if_fail (E_IS_SEARCH_BAR (search_bar)); + + button = GTK_TOGGLE_BUTTON (search_bar->priv->case_sensitive_button); + + gtk_toggle_button_set_active (button, case_sensitive); + + g_object_notify (G_OBJECT (search_bar), "case-sensitive"); +} + +gchar * +e_search_bar_get_text (ESearchBar *search_bar) +{ + GtkEntry *entry; + const gchar *text; + + g_return_val_if_fail (E_IS_SEARCH_BAR (search_bar), NULL); + + entry = GTK_ENTRY (search_bar->priv->entry); + text = gtk_entry_get_text (entry); + + return g_strstrip (g_strdup (text)); +} + +void +e_search_bar_set_text (ESearchBar *search_bar, + const gchar *text) +{ + GtkEntry *entry; + + g_return_if_fail (E_IS_SEARCH_BAR (search_bar)); + + entry = GTK_ENTRY (search_bar->priv->entry); + + if (text == NULL) + text = ""; + + /* This will trigger a "notify::text" signal. */ + gtk_entry_set_text (entry, text); +} diff --git a/widgets/misc/e-search-bar.h b/widgets/misc/e-search-bar.h new file mode 100644 index 0000000000..87e1023baf --- /dev/null +++ b/widgets/misc/e-search-bar.h @@ -0,0 +1,87 @@ +/* + * e-search-bar.h + * + * This program 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 License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see + * + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifndef E_SEARCH_BAR_H +#define E_SEARCH_BAR_H + +#include +#include +#include + +/* Standard GObject macros */ +#define E_TYPE_SEARCH_BAR \ + (e_search_bar_get_type ()) +#define E_SEARCH_BAR(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_SEARCH_BAR, ESearchBar)) +#define E_SEARCH_BAR_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_SEARCH_BAR, ESearchBarClass)) +#define E_IS_SEARCH_BAR(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_SEARCH_BAR)) +#define E_IS_SEARCH_BAR_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_SEARCH_BAR)) +#define E_SEARCH_BAR_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_SEARCH_BAR, ESearchBarClass)) + +G_BEGIN_DECLS + +typedef struct _ESearchBar ESearchBar; +typedef struct _ESearchBarClass ESearchBarClass; +typedef struct _ESearchBarPrivate ESearchBarPrivate; + +struct _ESearchBar { + GtkHBox parent; + ESearchBarPrivate *priv; +}; + +struct _ESearchBarClass { + GtkHBoxClass parent_class; + + /* Signals */ + void (*changed) (ESearchBar *search_bar); + void (*clear) (ESearchBar *search_bar); +}; + +GType e_search_bar_get_type (void); +GtkWidget * e_search_bar_new (EWebView *web_view); +void e_search_bar_clear (ESearchBar *search_bar); +void e_search_bar_changed (ESearchBar *search_bar); +EWebView * e_search_bar_get_web_view (ESearchBar *search_bar); +ESearchingTokenizer * + e_search_bar_get_tokenizer (ESearchBar *search_bar); +gboolean e_search_bar_get_active_search + (ESearchBar *search_bar); +gboolean e_search_bar_get_case_sensitive + (ESearchBar *search_bar); +void e_search_bar_set_case_sensitive + (ESearchBar *search_bar, + gboolean case_sensitive); +gchar * e_search_bar_get_text (ESearchBar *search_bar); +void e_search_bar_set_text (ESearchBar *search_bar, + const gchar *text); + +G_END_DECLS + +#endif /* E_SEARCH_BAR_H */ diff --git a/widgets/misc/e-searching-tokenizer.c b/widgets/misc/e-searching-tokenizer.c new file mode 100644 index 0000000000..8920a7dd21 --- /dev/null +++ b/widgets/misc/e-searching-tokenizer.c @@ -0,0 +1,1195 @@ +/* + * This program 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 License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see + * + * + * Authors: + * Developed by Jon Trowbridge + * Rewritten significantly to handle multiple strings and improve performance + * by Michael Zucchi + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include + +#include "e-searching-tokenizer.h" + +#include "libedataserver/e-memory.h" + +#define d(x) + +#define E_SEARCHING_TOKENIZER_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE \ + ((obj), E_TYPE_SEARCHING_TOKENIZER, ESearchingTokenizerPrivate)) + +enum { + MATCH_SIGNAL, + LAST_SIGNAL +}; + +static gpointer parent_class; +static guint signals[LAST_SIGNAL]; + +/* Utility functions */ + +/* This is faster and safer than glib2's utf8 abomination, but isn't exported from camel as yet */ +static inline guint32 +camel_utf8_getc(const guchar **ptr) +{ + register guchar *p = (guchar *)*ptr; + register guchar c, r; + register guint32 v, m; + +again: + r = *p++; +loop: + if (r < 0x80) { + *ptr = p; + v = r; + } else if (r < 0xfe) { /* valid start char? */ + v = r; + m = 0x7f80; /* used to mask out the length bits */ + do { + c = *p++; + if ((c & 0xc0) != 0x80) { + r = c; + goto loop; + } + v = (v<<6) | (c & 0x3f); + r<<=1; + m<<=5; + } while (r & 0x40); + + *ptr = p; + + v &= ~m; + } else { + goto again; + } + + return v; +} + +/* note: our tags of interest are 7 bit ascii, only, no need to do any fancy utf8 stuff */ +/* tags should be upper case + if this list gets longer than 10 entries, consider binary search */ +static const gchar *ignored_tags[] = { + "B", "I", "FONT", "TT", "EM", /* and more? */}; + +static gint +ignore_tag (const gchar *tag) +{ + gchar *t = g_alloca(strlen(tag)+1), c, *out; + const gchar *in; + gint i; + + /* we could use a aho-corasick matcher here too ... but we wont */ + + /* normalise tag into 't'. + Note we use the property that the only tags we're interested in + are 7 bit ascii to shortcut and simplify case insensitivity */ + in = tag+2; /* skip: TAG_ESCAPE '<' */ + if (*in == '/') + in++; + out = t; + while ((c = *in++)) { + if (c >= 'A' && c <= 'Z') + *out++ = c; + else if (c >= 'a' && c <= 'z') + *out++ = c & 0xdf; /* convert ASCII to upper case */ + else + /* maybe should check for > or ' ' etc? */ + break; + } + *out = 0; + + for (i = 0; i < G_N_ELEMENTS (ignored_tags); i++) { + if (strcmp (t, ignored_tags[i]) == 0) + return 1; + } + + return 0; +} + +/* ********************************************************************** */ + +/* Aho-Corasick search tree implmeentation */ + +/* next state if we match a character */ +struct _match { + struct _match *next; + guint32 ch; + struct _state *match; +}; + +/* tree state node */ +struct _state { + struct _match *matches; + guint final; /* max no of chars we just matched */ + struct _state *fail; /* where to try next if we fail */ + struct _state *next; /* next on this level? */ +}; + +/* base tree structure */ +struct _trie { + struct _state root; + gint max_depth; + + EMemChunk *state_chunks; + EMemChunk *match_chunks; +}; + +/* will be enabled only if debug is enabled */ +#if d(1) -1 != -1 +static void +dump_trie (struct _state *s, gint d) +{ + gchar *p = g_alloca(d*2+1); + struct _match *m; + + memset(p, ' ', d*2); + p[d*2]=0; + + printf("%s[state] %p: %d fail->%p\n", p, s, s->final, s->fail); + m = s->matches; + while (m) { + printf(" %s'%c' -> %p\n", p, m->ch, m->match); + if (m->match) + dump_trie (m->match, d+1); + m = m->next; + } +} +#endif + +/* This builds an Aho-Corasick search trie for a set of utf8 words */ +/* See + http://www-sr.informatik.uni-tuebingen.de/~buehler/AC/AC.html + for a neat demo */ + +static inline struct _match * +g (struct _state *q, guint32 c) +{ + struct _match *m = q->matches; + + while (m && m->ch != c) + m = m->next; + + return m; +} + +static struct _trie * +build_trie (gint nocase, gint len, guchar **words) +{ + struct _state *q, *qt, *r; + const guchar *word; + struct _match *m, *n = NULL; + gint i, depth; + guint32 c; + struct _trie *trie; + gint state_depth_max, state_depth_size; + struct _state **state_depth; + + trie = g_malloc(sizeof(*trie)); + trie->root.matches = NULL; + trie->root.final = 0; + trie->root.fail = NULL; + trie->root.next = NULL; + + trie->state_chunks = e_memchunk_new (8, sizeof(struct _state)); + trie->match_chunks = e_memchunk_new (8, sizeof(struct _match)); + + /* This will correspond to the length of the longest pattern */ + state_depth_size = 0; + state_depth_max = 64; + state_depth = g_malloc(sizeof(*state_depth[0])*64); + state_depth[0] = NULL; + + /* Step 1: Build trie */ + + /* This just builds a tree that merges all common prefixes into the same branch */ + + for (i=0;iroot; + depth = 0; + while ((c = camel_utf8_getc (&word))) { + if (nocase) + c = g_unichar_tolower (c); + m = g (q, c); + if (m == NULL) { + m = e_memchunk_alloc(trie->match_chunks); + m->ch = c; + m->next = q->matches; + q->matches = m; + q = m->match = e_memchunk_alloc(trie->state_chunks); + q->matches = NULL; + q->fail = &trie->root; + q->final = 0; + if (state_depth_max < depth) { + state_depth_max += 64; + state_depth = g_realloc(state_depth, sizeof(*state_depth[0])*state_depth_max); + } + if (state_depth_size < depth) { + state_depth[depth] = NULL; + state_depth_size = depth; + } + q->next = state_depth[depth]; + state_depth[depth] = q; + } else { + q = m->match; + } + depth++; + } + q->final = depth; + } + + d(printf("Dumping trie:\n")); + d(dump_trie (&trie->root, 0)); + + /* Step 2: Build failure graph */ + + /* This searches for the longest substring which is a prefix of another string and + builds a graph of failure links so you can find multiple substrings concurrently, + using aho-corasick's algorithm */ + + for (i=0;imatches; + while (m) { + c = m->ch; + qt = m->match; + r = q->fail; + while (r && (n = g (r, c)) == NULL) + r = r->fail; + if (r != NULL) { + qt->fail = n->match; + if (qt->fail->final > qt->final) + qt->final = qt->fail->final; + } else { + if ((n = g (&trie->root, c))) + qt->fail = n->match; + else + qt->fail = &trie->root; + } + m = m->next; + } + q = q->next; + } + } + + d (printf("After failure analysis\n")); + d (dump_trie (&trie->root, 0)); + + g_free (state_depth); + + trie->max_depth = state_depth_size; + + return trie; +} + +static void +free_trie (struct _trie *t) +{ + e_memchunk_destroy(t->match_chunks); + e_memchunk_destroy(t->state_chunks); + + g_free (t); +} + +/* ********************************************************************** */ + +/* html token searcher */ + +struct _token { + struct _token *next; + struct _token *prev; + guint offset; + /* we need to copy the token for memory management, so why not copy it whole */ + gchar tok[1]; +}; + +/* stack of submatches currently being scanned, used for merging */ +struct _submatch { + guint offstart, offend; /* in bytes */ +}; + +/* flags for new func */ +#define SEARCH_CASE (1) +#define SEARCH_BOLD (2) + +struct _searcher { + struct _trie *t; + + gchar *(*next_token)(); /* callbacks for more tokens */ + gpointer next_data; + + gint words; /* how many words */ + gchar *tags, *tage; /* the tag we used to highlight */ + + gint flags; /* case sensitive or not */ + + struct _state *state; /* state is the current trie state */ + + gint matchcount; + + GQueue input; /* pending 'input' tokens, processed but might match */ + GQueue output; /* output tokens ready for source */ + + struct _token *current; /* for token output memory management */ + + guint32 offset; /* current offset through searchable stream? */ + guint32 offout; /* last output position */ + + guint lastp; /* current position in rotating last buffer */ + guint32 *last; /* buffer that goes back last 'n' positions */ + guint32 last_mask; /* bitmask for efficient rotation calculation */ + + guint submatchp; /* submatch stack */ + struct _submatch *submatches; +}; + +static void +searcher_set_tokenfunc(struct _searcher *s, gchar *(*next)(), gpointer data) +{ + s->next_token = next; + s->next_data = data; +} + +static struct _searcher * +searcher_new (gint flags, gint argc, guchar **argv, const gchar *tags, const gchar *tage) +{ + gint i, m; + struct _searcher *s; + + s = g_malloc(sizeof(*s)); + + s->t = build_trie ((flags&SEARCH_CASE) == 0, argc, argv); + s->words = argc; + s->tags = g_strdup (tags); + s->tage = g_strdup (tage); + s->flags = flags; + s->state = &s->t->root; + s->matchcount = 0; + + g_queue_init (&s->input); + g_queue_init (&s->output); + s->current = NULL; + + s->offset = 0; + s->offout = 0; + + /* rotating queue of previous character positions */ + m = s->t->max_depth+1; + i = 2; + while (ilast = g_malloc(sizeof(s->last[0])*i); + s->last_mask = i-1; + s->lastp = 0; + + /* a stack of possible submatches */ + s->submatchp = 0; + s->submatches = g_malloc(sizeof(s->submatches[0])*argc+1); + + return s; +} + +static void +searcher_free (struct _searcher *s) +{ + struct _token *t; + + while ((t = g_queue_pop_head (&s->input)) != NULL) + g_free (t); + while ((t = g_queue_pop_head (&s->output)) != NULL) + g_free (t); + g_free (s->tags); + g_free (s->tage); + g_free (s->last); + g_free (s->submatches); + free_trie (s->t); + g_free (s); +} + +static struct _token * +append_token (GQueue *queue, const gchar *tok, gint len) +{ + struct _token *token; + + if (len == -1) + len = strlen(tok); + token = g_malloc(sizeof(*token) + len+1); + token->offset = 0; /* set by caller when required */ + memcpy(token->tok, tok, len); + token->tok[len] = 0; + g_queue_push_tail (queue, token); + + return token; +} + +#define free_token(x) (g_free (x)) + +static void +output_token(struct _searcher *s, struct _token *token) +{ + gint offend; + gint left, pre; + + if (token->tok[0] == TAG_ESCAPE) { + if (token->offset >= s->offout) { + d (printf("moving tag token '%s' from input to output\n", token->tok[0]==TAG_ESCAPE?token->tok+1:token->tok)); + g_queue_push_tail (&s->output, token); + } else { + d (printf("discarding tag token '%s' from input\n", token->tok[0]==TAG_ESCAPE?token->tok+1:token->tok)); + free_token(token); + } + } else { + offend = token->offset + strlen(token->tok); + left = offend-s->offout; + if (left > 0) { + pre = s->offout - token->offset; + if (pre>0) + memmove (token->tok, token->tok+pre, left+1); + d (printf("adding partial remaining/failed '%s'\n", token->tok[0]==TAG_ESCAPE?token->tok+1:token->tok)); + s->offout = offend; + g_queue_push_tail (&s->output, token); + } else { + d (printf("discarding whole token '%s'\n", token->tok[0]==TAG_ESCAPE?token->tok+1:token->tok)); + free_token(token); + } + } +} + +static struct _token * +find_token(struct _searcher *s, gint start) +{ + GList *link; + + /* find token which is start token, from end of list back */ + link = g_queue_peek_tail_link (&s->input); + while (link != NULL) { + struct _token *token = link->data; + + if (token->offset <= start) + return token; + + link = g_list_previous (link); + } + + return NULL; +} + +static void +output_match(struct _searcher *s, guint start, guint end) +{ + register struct _token *token; + struct _token *starttoken, *endtoken; + gchar b[8]; + + d (printf("output match: %d-%d at %d\n", start, end, s->offout)); + + starttoken = find_token(s, start); + endtoken = find_token(s, end); + + if (starttoken == NULL || endtoken == NULL) { + d (printf("Cannot find match history for match %d-%d\n", start, end)); + return; + } + + d (printf("start in token '%s'\n", starttoken->tok[0]==TAG_ESCAPE?starttoken->tok+1:starttoken->tok)); + d (printf("end in token '%s'\n", endtoken->tok[0]==TAG_ESCAPE?endtoken->tok+1:endtoken->tok)); + + /* output pending stuff that didn't match afterall */ + while ((struct _token *) g_queue_peek_head (&s->input) != starttoken) { + token = g_queue_pop_head (&s->input); + d (printf("appending failed match '%s'\n", token->tok[0]==TAG_ESCAPE?token->tok+1:token->tok)); + output_token(s, token); + } + + /* output any pre-match text */ + if (s->offout < start) { + token = append_token(&s->output, starttoken->tok + (s->offout-starttoken->offset), start-s->offout); + d (printf("adding pre-match text '%s'\n", token->tok[0]==TAG_ESCAPE?token->tok+1:token->tok)); + s->offout = start; + } + + /* output highlight/bold */ + if (s->flags & SEARCH_BOLD) { + sprintf(b, "%c", (gchar)TAG_ESCAPE); + append_token(&s->output, b, -1); + } + if (s->tags) + append_token(&s->output, s->tags, -1); + + /* output match node (s) */ + if (starttoken != endtoken) { + while ((struct _token *) g_queue_peek_head (&s->input) != endtoken) { + token = g_queue_pop_head (&s->input); + d (printf("appending (partial) match node (head) '%s'\n", token->tok[0]==TAG_ESCAPE?token->tok+1:token->tok)); + output_token(s, token); + } + } + + /* any remaining partial content */ + if (s->offout < end) { + token = append_token(&s->output, endtoken->tok+(s->offout-endtoken->offset), end-s->offout); + d (printf("appending (partial) match node (tail) '%s'\n", token->tok[0]==TAG_ESCAPE?token->tok+1:token->tok)); + s->offout = end; + } + + /* end highlight */ + if (s->tage) + append_token(&s->output, s->tage, -1); + + /* and close bold if we need to */ + if (s->flags & SEARCH_BOLD) { + sprintf(b, "%c", (gchar)TAG_ESCAPE); + append_token(&s->output, b, -1); + } +} + +/* output any sub-pending blocks */ +static void +output_subpending (struct _searcher *s) +{ + gint i; + + for (i=s->submatchp-1;i>=0;i--) + output_match(s, s->submatches[i].offstart, s->submatches[i].offend); + s->submatchp = 0; +} + +/* returns true if a merge took place */ +static gint +merge_subpending (struct _searcher *s, gint offstart, gint offend) +{ + gint i; + + /* merges overlapping or abutting match strings */ + if (s->submatchp && + s->submatches[s->submatchp-1].offend >= offstart) { + + /* go from end, any that match 'invalidate' follow-on ones too */ + for (i=s->submatchp-1;i>=0;i--) { + if (s->submatches[i].offend >= offstart) { + if (offstart < s->submatches[i].offstart) + s->submatches[i].offstart = offstart; + s->submatches[i].offend = offend; + if (s->submatchp > i) + s->submatchp = i+1; + } + } + return 1; + } + + return 0; +} + +static void +push_subpending (struct _searcher *s, gint offstart, gint offend) +{ + /* This is really an assertion, we just ignore the last pending match instead of crashing though */ + if (s->submatchp >= s->words) { + d (printf("ERROR: submatch pending stack overflow\n")); + s->submatchp = s->words-1; + } + + s->submatches[s->submatchp].offstart = offstart; + s->submatches[s->submatchp].offend = offend; + s->submatchp++; +} + +/* move any (partial) tokens from input to output if they are beyond the current output position */ +static void +output_pending (struct _searcher *s) +{ + struct _token *token; + + while ((token = g_queue_pop_head (&s->input)) != NULL) + output_token(s, token); +} + +/* flushes any nodes we cannot possibly match anymore */ +static void +flush_extra(struct _searcher *s) +{ + guint start; + gint i; + struct _token *starttoken, *token; + + /* find earliest gchar that can be in contention */ + start = s->offset - s->t->max_depth; + for (i=0;isubmatchp;i++) + if (s->submatches[i].offstart < start) + start = s->submatches[i].offstart; + + /* now, flush out any tokens which are before this point */ + starttoken = find_token(s, start); + if (starttoken == NULL) + return; + + while ((struct _token *) g_queue_peek_head (&s->input) != starttoken) { + token = g_queue_pop_head (&s->input); + output_token(s, token); + } +} + +static gchar * +searcher_next_token(struct _searcher *s) +{ + struct _token *token; + const guchar *tok, *stok, *pre_tok; + struct _trie *t = s->t; + struct _state *q = s->state; + struct _match *m = NULL; + gint offstart, offend; + guint32 c; + + while (g_queue_is_empty (&s->output)) { + /* get next token */ + tok = (guchar *)s->next_token(s->next_data); + if (tok == NULL) { + output_subpending (s); + output_pending (s); + break; + } + + /* we dont always have to copy each token, e.g. if we dont match anything */ + token = append_token(&s->input, (gchar *)tok, -1); + token->offset = s->offset; + tok = (guchar *)token->tok; + + d (printf("new token %d '%s'\n", token->offset, token->tok[0]==TAG_ESCAPE?token->tok+1:token->tok)); + + /* tag test, reset state on unknown tags */ + if (tok[0] == TAG_ESCAPE) { + if (!ignore_tag ((gchar *)tok)) { + /* force reset */ + output_subpending (s); + output_pending (s); + q = &t->root; + } + + continue; + } + + /* process whole token */ + pre_tok = stok = tok; + while ((c = camel_utf8_getc (&tok))) { + if ((s->flags & SEARCH_CASE) == 0) + c = g_unichar_tolower (c); + while (q && (m = g (q, c)) == NULL) + q = q->fail; + if (q == NULL) { + /* mismatch ... reset state */ + output_subpending (s); + q = &t->root; + } else if (m != NULL) { + /* keep track of previous offsets of utf8 chars, rotating buffer */ + s->last[s->lastp] = s->offset + (pre_tok-stok); + s->lastp = (s->lastp+1)&s->last_mask; + + q = m->match; + /* we have a match of q->final characters for a matching word */ + if (q->final) { + s->matchcount++; + + /* use the last buffer to find the real offset of this gchar */ + offstart = s->last[(s->lastp - q->final)&s->last_mask]; + offend = s->offset + (tok - stok); + + if (q->matches == NULL) { + if (s->submatchp == 0) { + /* nothing pending, always put something in so we can try merge */ + push_subpending (s, offstart, offend); + } else if (!merge_subpending (s, offstart, offend)) { + /* can't merge, output what we have, and start againt */ + output_subpending (s); + push_subpending (s, offstart, offend); + /*output_match(s, offstart, offend);*/ + } else if (g_queue_get_length (&s->input) > 8) { + /* we're continuing to match and merge, but we have a lot of stuff + waiting, so flush it out now since this is a safe point to do it */ + output_subpending (s); + } + } else { + /* merge/add subpending */ + if (!merge_subpending (s, offstart, offend)) + push_subpending (s, offstart, offend); + } + } + } + pre_tok = tok; + } + + s->offset += (pre_tok-stok); + + flush_extra(s); + } + + s->state = q; + + if (s->current) + free_token(s->current); + + s->current = token = g_queue_pop_head (&s->output); + + return token ? g_strdup (token->tok) : NULL; +} + +static gchar * +searcher_peek_token(struct _searcher *s) +{ + gchar *tok; + + /* we just get it and then put it back, it's fast enuf */ + tok = searcher_next_token(s); + if (tok) { + /* need to clear this so we dont free it while its still active */ + g_queue_push_head (&s->output, s->current); + s->current = NULL; + } + + return tok; +} + +static gint +searcher_pending (struct _searcher *s) +{ + return !(g_queue_is_empty (&s->input) && g_queue_is_empty (&s->output)); +} + +/* ********************************************************************** */ + +struct _search_info { + GPtrArray *strv; + gchar *color; + guint size:8; + guint flags:8; +}; + +/** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** **/ + +static struct _search_info * +search_info_new (void) +{ + struct _search_info *s; + + s = g_malloc0(sizeof(struct _search_info)); + s->strv = g_ptr_array_new (); + + return s; +} + +static void +search_info_set_flags(struct _search_info *si, guint flags, guint mask) +{ + si->flags = (si->flags & ~mask) | (flags & mask); +} + +static void +search_info_set_color (struct _search_info *si, const gchar *color) +{ + g_free (si->color); + si->color = g_strdup (color); +} + +static void +search_info_add_string (struct _search_info *si, const gchar *s) +{ + const guchar *start; + guint32 c; + + if (s && s[0]) { + const guchar *us = (guchar *) s; + /* strip leading whitespace */ + start = us; + while ((c = camel_utf8_getc (&us))) { + if (!g_unichar_isspace (c)) { + break; + } + start = us; + } + /* should probably also strip trailing, but i'm lazy today */ + if (start[0]) + g_ptr_array_add (si->strv, g_strdup ((gchar *)start)); + } +} + +static void +search_info_clear (struct _search_info *si) +{ + gint i; + + for (i=0;istrv->len;i++) + g_free (si->strv->pdata[i]); + + g_ptr_array_set_size (si->strv, 0); +} + +static void +search_info_free (struct _search_info *si) +{ + gint i; + + for (i=0;istrv->len;i++) + g_free (si->strv->pdata[i]); + + g_ptr_array_free (si->strv, TRUE); + g_free (si->color); + g_free (si); +} + +static struct _search_info * +search_info_clone (struct _search_info *si) +{ + struct _search_info *out; + gint i; + + out = search_info_new (); + for (i=0;istrv->len;i++) + g_ptr_array_add (out->strv, g_strdup (si->strv->pdata[i])); + out->color = g_strdup (si->color); + out->flags = si->flags; + out->size = si->size; + + return out; +} + +static struct _searcher * +search_info_to_searcher (struct _search_info *si) +{ + gchar *tags, *tage; + const gchar *col; + + if (si->strv->len == 0) + return NULL; + + if (si->color == NULL) + col = "red"; + else + col = si->color; + + tags = g_alloca(20+strlen(col)); + sprintf(tags, "%c", TAG_ESCAPE, col); + tage = g_alloca(20); + sprintf(tage, "%c", TAG_ESCAPE); + + return searcher_new (si->flags, si->strv->len, (guchar **)si->strv->pdata, tags, tage); +} + +/* ********************************************************************** */ + +struct _ESearchingTokenizerPrivate { + struct _search_info *primary, *secondary; + struct _searcher *engine; +}; + +/** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** **/ + +/** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** **/ + +/* blah blah the htmltokeniser doesn't like being asked + for a token if it doens't hvae any! */ +static gchar * +get_token (HTMLTokenizer *tokenizer) +{ + HTMLTokenizerClass *class = HTML_TOKENIZER_CLASS (parent_class); + + if (class->has_more (tokenizer)) + return class->next_token (tokenizer); + + return NULL; +} + +/* proxy matched event, not sure what its for otherwise */ +static void +matched (ESearchingTokenizer *tokenizer) +{ + /*++tokenizer->priv->match_count;*/ + g_signal_emit (tokenizer, signals[MATCH_SIGNAL], 0); +} + +/* ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** */ + +static void +searching_tokenizer_finalize (GObject *object) +{ + ESearchingTokenizerPrivate *priv; + + priv = E_SEARCHING_TOKENIZER_GET_PRIVATE (object); + + search_info_free (priv->primary); + search_info_free (priv->secondary); + + if (priv->engine != NULL) + searcher_free (priv->engine); + + /* Chain up to parent's finalize () method. */ + G_OBJECT_CLASS (parent_class)->finalize (object); +} + +static void +searching_tokenizer_begin (HTMLTokenizer *tokenizer, + const gchar *content_type) +{ + ESearchingTokenizerPrivate *priv; + + priv = E_SEARCHING_TOKENIZER_GET_PRIVATE (tokenizer); + + /* reset search */ + if (priv->engine != NULL) { + searcher_free (priv->engine); + priv->engine = NULL; + } + + if ((priv->engine = search_info_to_searcher (priv->primary)) + || (priv->engine = search_info_to_searcher (priv->secondary))) { + searcher_set_tokenfunc(priv->engine, get_token, tokenizer); + } + /* else - no engine, no search active */ + + /* Chain up to parent's begin() method. */ + HTML_TOKENIZER_CLASS (parent_class)->begin (tokenizer, content_type); +} + +static gchar * +searching_tokenizer_peek_token (HTMLTokenizer *tokenizer) +{ + ESearchingTokenizerPrivate *priv; + + priv = E_SEARCHING_TOKENIZER_GET_PRIVATE (tokenizer); + + if (priv->engine != NULL) + return searcher_peek_token (priv->engine); + + /* Chain up to parent's peek_token() method. */ + return HTML_TOKENIZER_CLASS (parent_class)->peek_token (tokenizer); +} + +static gchar * +searching_tokenizer_next_token (HTMLTokenizer *tokenizer) +{ + ESearchingTokenizerPrivate *priv; + gint oldmatched; + gchar *token; + + priv = E_SEARCHING_TOKENIZER_GET_PRIVATE (tokenizer); + + /* If no search is active, just use the default method. */ + if (priv->engine == NULL) + return HTML_TOKENIZER_CLASS (parent_class)->next_token (tokenizer); + + oldmatched = priv->engine->matchcount; + + token = searcher_next_token (priv->engine); + + /* not sure if this has to be accurate or just say we had some matches */ + if (oldmatched != priv->engine->matchcount) + g_signal_emit (tokenizer, signals[MATCH_SIGNAL], 0); + + return token; +} + +static gboolean +searching_tokenizer_has_more (HTMLTokenizer *tokenizer) +{ + ESearchingTokenizerPrivate *priv; + + priv = E_SEARCHING_TOKENIZER_GET_PRIVATE (tokenizer); + + return (priv->engine != NULL && searcher_pending (priv->engine)) || + HTML_TOKENIZER_CLASS (parent_class)->has_more (tokenizer); +} + +static HTMLTokenizer * +searching_tokenizer_clone (HTMLTokenizer *tokenizer) +{ + ESearchingTokenizer *orig_st; + ESearchingTokenizer *new_st; + + orig_st = E_SEARCHING_TOKENIZER (tokenizer); + new_st = e_searching_tokenizer_new (); + + search_info_free (new_st->priv->primary); + search_info_free (new_st->priv->secondary); + + new_st->priv->primary = search_info_clone (orig_st->priv->primary); + new_st->priv->secondary = search_info_clone (orig_st->priv->secondary); + + g_signal_connect_swapped ( + new_st, "match", G_CALLBACK (matched), orig_st); + + return HTML_TOKENIZER (new_st); +} +static void +searching_tokenizer_class_init (ESearchingTokenizerClass *class) +{ + GObjectClass *object_class; + HTMLTokenizerClass *tokenizer_class; + + parent_class = g_type_class_peek_parent (class); + g_type_class_add_private (class, sizeof (ESearchingTokenizerPrivate)); + + object_class = G_OBJECT_CLASS (class); + object_class->finalize = searching_tokenizer_finalize; + + tokenizer_class = HTML_TOKENIZER_CLASS (class); + tokenizer_class->begin = searching_tokenizer_begin; + tokenizer_class->peek_token = searching_tokenizer_peek_token; + tokenizer_class->next_token = searching_tokenizer_next_token; + tokenizer_class->has_more = searching_tokenizer_has_more; + tokenizer_class->clone = searching_tokenizer_clone; + + signals[MATCH_SIGNAL] = g_signal_new ( + "match", + G_TYPE_FROM_CLASS (class), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ESearchingTokenizerClass, match), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); +} + +static void +searching_tokenizer_init (ESearchingTokenizer *tokenizer) +{ + tokenizer->priv = E_SEARCHING_TOKENIZER_GET_PRIVATE (tokenizer); + + tokenizer->priv->primary = search_info_new (); + search_info_set_flags ( + tokenizer->priv->primary, + SEARCH_BOLD, SEARCH_CASE | SEARCH_BOLD); + search_info_set_color (tokenizer->priv->primary, "red"); + + tokenizer->priv->secondary = search_info_new (); + search_info_set_flags( + tokenizer->priv->secondary, + SEARCH_BOLD, SEARCH_CASE | SEARCH_BOLD); + search_info_set_color (tokenizer->priv->secondary, "purple"); +} + +GType +e_searching_tokenizer_get_type (void) +{ + static GType type = 0; + + if (G_UNLIKELY (type == 0)) { + static const GTypeInfo type_info = { + sizeof (ESearchingTokenizerClass), + (GBaseInitFunc) NULL, + (GBaseFinalizeFunc) NULL, + (GClassInitFunc) searching_tokenizer_class_init, + (GClassFinalizeFunc) NULL, + NULL, /* class_data */ + sizeof (ESearchingTokenizer), + 0, /* n_preallocs */ + (GInstanceInitFunc) searching_tokenizer_init, + NULL /* value_table */ + }; + + type = g_type_register_static ( + HTML_TYPE_TOKENIZER, "ESearchingTokenizer", + &type_info, 0); + } + + return type; +} + +ESearchingTokenizer * +e_searching_tokenizer_new (void) +{ + return g_object_new (E_TYPE_SEARCHING_TOKENIZER, NULL); +} + +void +e_searching_tokenizer_set_primary_search_string (ESearchingTokenizer *tokenizer, + const gchar *primary_string) +{ + g_return_if_fail (E_IS_SEARCHING_TOKENIZER (tokenizer)); + + search_info_clear (tokenizer->priv->primary); + search_info_add_string (tokenizer->priv->primary, primary_string); +} + +void +e_searching_tokenizer_add_primary_search_string (ESearchingTokenizer *tokenizer, + const gchar *primary_string) +{ + g_return_if_fail (E_IS_SEARCHING_TOKENIZER (tokenizer)); + + search_info_add_string (tokenizer->priv->primary, primary_string); +} + +void +e_searching_tokenizer_set_primary_case_sensitivity (ESearchingTokenizer *tokenizer, + gboolean case_sensitive) +{ + g_return_if_fail (E_IS_SEARCHING_TOKENIZER (tokenizer)); + + search_info_set_flags ( + tokenizer->priv->primary, + case_sensitive ? SEARCH_CASE : 0, SEARCH_CASE); +} + +void +e_searching_tokenizer_set_secondary_search_string (ESearchingTokenizer *tokenizer, + const gchar *secondary_string) +{ + g_return_if_fail (E_IS_SEARCHING_TOKENIZER (tokenizer)); + + search_info_clear (tokenizer->priv->secondary); + search_info_add_string (tokenizer->priv->secondary, secondary_string); +} + +void +e_searching_tokenizer_add_secondary_search_string (ESearchingTokenizer *tokenizer, + const gchar *secondary_string) +{ + g_return_if_fail (E_IS_SEARCHING_TOKENIZER (tokenizer)); + + search_info_add_string (tokenizer->priv->secondary, secondary_string); +} + +void +e_searching_tokenizer_set_secondary_case_sensitivity (ESearchingTokenizer *tokenizer, + gboolean case_sensitive) +{ + g_return_if_fail (E_IS_SEARCHING_TOKENIZER (tokenizer)); + + search_info_set_flags ( + tokenizer->priv->secondary, + case_sensitive ? SEARCH_CASE : 0, SEARCH_CASE); +} + +/* Note: only returns the primary search string count */ +gint +e_searching_tokenizer_match_count (ESearchingTokenizer *tokenizer) +{ + g_return_val_if_fail (E_IS_SEARCHING_TOKENIZER (tokenizer), -1); + + if (tokenizer->priv->engine && tokenizer->priv->primary->strv->len) + return tokenizer->priv->engine->matchcount; + + return 0; +} diff --git a/widgets/misc/e-searching-tokenizer.h b/widgets/misc/e-searching-tokenizer.h new file mode 100644 index 0000000000..7eb603a25d --- /dev/null +++ b/widgets/misc/e-searching-tokenizer.h @@ -0,0 +1,92 @@ +/* + * + * This program 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 License, or (at your option) version 3. + * + * 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with the program; if not, see + * + * + * Authors: + * Jon Trowbridge + * + * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) + * + */ + +#ifndef E_SEARCHING_TOKENIZER_H +#define E_SEARCHING_TOKENIZER_H + +#include +#include + +/* Standard GObject macros */ +#define E_TYPE_SEARCHING_TOKENIZER \ + (e_searching_tokenizer_get_type ()) +#define E_SEARCHING_TOKENIZER(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST \ + ((obj), E_TYPE_SEARCHING_TOKENIZER, ESearchingTokenizer)) +#define E_SEARCHING_TOKENIZER_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_CAST \ + ((cls), E_TYPE_SEARCHING_TOKENIZER, ESearchingTokenizerClass)) +#define E_IS_SEARCHING_TOKENIZER(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE \ + ((obj), E_TYPE_SEARCHING_TOKENIZER)) +#define E_IS_SEARCHING_TOKENIZER_CLASS(cls) \ + (G_TYPE_CHECK_CLASS_TYPE \ + ((cls), E_TYPE_SEARCHING_TOKENIZER)) +#define E_SEARCH_TOKENIZER_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS \ + ((obj), E_TYPE_SEARCHING_TOKENIZER, ESearchingTokenizerClass)) + +G_BEGIN_DECLS + +typedef struct _ESearchingTokenizer ESearchingTokenizer; +typedef struct _ESearchingTokenizerClass ESearchingTokenizerClass; +typedef struct _ESearchingTokenizerPrivate ESearchingTokenizerPrivate; + +struct _ESearchingTokenizer { + HTMLTokenizer parent; + ESearchingTokenizerPrivate *priv; +}; + +struct _ESearchingTokenizerClass { + HTMLTokenizerClass parent_class; + + void (*match) (ESearchingTokenizer *tokenizer); +}; + +GType e_searching_tokenizer_get_type (void); +ESearchingTokenizer * + e_searching_tokenizer_new (void); +void e_searching_tokenizer_set_primary_search_string + (ESearchingTokenizer *tokenizer, + const gchar *primary_string); +void e_searching_tokenizer_add_primary_search_string + (ESearchingTokenizer *tokenizer, + const gchar *primary_string); +void e_searching_tokenizer_set_primary_case_sensitivity + (ESearchingTokenizer *tokenizer, + gboolean case_sensitive); +void e_searching_tokenizer_set_secondary_search_string + (ESearchingTokenizer *tokenizer, + const gchar *secondary_string); +void e_searching_tokenizer_add_secondary_search_string + (ESearchingTokenizer *tokenizer, + const gchar *secondary_string); +void e_searching_tokenizer_set_secondary_case_sensitivity + (ESearchingTokenizer *tokenizer, + gboolean case_sensitive); +gint e_searching_tokenizer_match_count + (ESearchingTokenizer *tokenizer); + +G_END_DECLS + +#endif /* E_SEARCHING_TOKENIZER_H */ -- cgit v1.2.3