/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ /* * Copyright (C) 2001-2003 CodeFactory AB * Copyright (C) 2001-2003 Mikael Hallendal * Copyright (C) 2005-2006 Imendio AB * * 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, write to the * Free Software Foundation, Inc., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. */ #include #include #include #include #include #include #include "dh-marshal.h" #include "dh-keyword-model.h" #include "dh-search.h" #include "dh-preferences.h" #include "dh-base.h" #define d(x) typedef enum _DhSearchSource DhSearchSource; enum _DhSearchSource { SEARCH_API = 0, SEARCH_ENTRY }; struct _DhSearchPriv { DhKeywordModel *model; DhLink *selected_link; GtkWidget *advanced_box; GtkWidget *book; GtkWidget *page; GtkWidget *entry; GtkWidget *hitlist; GCompletion *completion; guint idle_complete; guint idle_filter; gboolean first; guint advanced_options_id; GString *book_str; GString *page_str; GString *entry_str; DhSearchSource search_source; }; static void search_init (DhSearch *search); static void search_class_init (DhSearchClass *klass); static void search_finalize (GObject *object); static void search_advanced_options_setup (DhSearch *search); static void search_advanced_options_notify_cb (GConfClient *client, guint cnxn_id, GConfEntry *entry, gpointer user_data); static void search_selection_changed_cb (GtkTreeSelection *selection, DhSearch *content); static gboolean search_tree_button_press_cb (GtkTreeView *view, GdkEventButton *event, DhSearch *search); static gboolean search_entry_key_press_event_cb (GtkEntry *entry, GdkEventKey *event, DhSearch *search); static void search_entry_changed_cb (GtkEntry *entry, DhSearch *search); static void search_entry_activated_cb (GtkEntry *entry, DhSearch *search); static void search_entry_text_inserted_cb (GtkEntry *entry, const gchar *text, gint length, gint *position, DhSearch *search); static gboolean search_complete_idle (DhSearch *search); static gboolean search_filter_idle (DhSearch *search); static gchar * search_complete_func (DhLink *link); static gchar * search_get_search_string (DhSearch *search); enum { LINK_SELECTED, LAST_SIGNAL }; static GtkVBox *parent_class; static gint signals[LAST_SIGNAL] = { 0 }; GType dh_search_get_type (void) { static GType type = 0; if (!type) { static const GTypeInfo info = { sizeof (DhSearchClass), NULL, NULL, (GClassInitFunc) search_class_init, NULL, NULL, sizeof (DhSearch), 0, (GInstanceInitFunc) search_init, }; type = g_type_register_static (GTK_TYPE_VBOX, "DhSearch", &info, 0); } return type; } static void search_class_init (DhSearchClass *klass) { GObjectClass *object_class; GtkWidgetClass *widget_class; parent_class = g_type_class_peek_parent (klass); object_class = (GObjectClass *) klass; widget_class = (GtkWidgetClass *) klass; object_class->finalize = search_finalize; signals[LINK_SELECTED] = g_signal_new ("link_selected", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (DhSearchClass, link_selected), NULL, NULL, dh_marshal_VOID__POINTER, G_TYPE_NONE, 1, G_TYPE_POINTER); } static void search_init (DhSearch *search) { DhSearchPriv *priv; priv = g_new0 (DhSearchPriv, 1); search->priv = priv; priv->book_str = g_string_new (""); priv->page_str = g_string_new (""); priv->entry_str = g_string_new (""); priv->idle_complete = 0; priv->idle_filter = 0; priv->completion = g_completion_new ((GCompletionFunc) search_complete_func); priv->hitlist = gtk_tree_view_new (); priv->model = dh_keyword_model_new (); gtk_tree_view_set_model (GTK_TREE_VIEW (priv->hitlist), GTK_TREE_MODEL (priv->model)); gtk_tree_view_set_enable_search (GTK_TREE_VIEW (priv->hitlist), FALSE); gtk_box_set_spacing (GTK_BOX (search), 2); } static void search_finalize (GObject *object) { DhSearchPriv *priv; GConfClient *gconf_client; priv = DH_SEARCH (object)->priv; g_string_free (priv->book_str, TRUE); g_string_free (priv->page_str, TRUE); g_string_free (priv->entry_str, TRUE); g_completion_free (priv->completion); gconf_client = dh_base_get_gconf_client (dh_base_get ()); gconf_client_notify_remove (gconf_client, priv->advanced_options_id); G_OBJECT_CLASS (parent_class)->finalize (object); } static void search_advanced_options_setup (DhSearch *search) { DhSearchPriv *priv; gboolean advanced_options; GConfClient *gconf_client; priv = search->priv; gconf_client = dh_base_get_gconf_client (dh_base_get ()); advanced_options = gconf_client_get_bool (gconf_client, GCONF_ADVANCED_OPTIONS, NULL); if (advanced_options) { gtk_widget_show (priv->advanced_box); g_signal_handlers_block_by_func (priv->book, search_entry_changed_cb, search); g_signal_handlers_block_by_func (priv->page, search_entry_changed_cb, search); gtk_entry_set_text (GTK_ENTRY (priv->book), priv->book_str->len > 5 ? priv->book_str->str + 5 : ""); gtk_entry_set_text (GTK_ENTRY (priv->page), priv->page_str->len > 5 ? priv->page_str->str + 5 : ""); g_signal_handlers_unblock_by_func (priv->book, search_entry_changed_cb, search); g_signal_handlers_unblock_by_func (priv->page, search_entry_changed_cb, search); } else { gtk_widget_hide (priv->advanced_box); } } static void search_advanced_options_notify_cb (GConfClient *client, guint cnxn_id, GConfEntry *entry, gpointer user_data) { DhSearch *search; DhSearchPriv *priv; search = DH_SEARCH (user_data); priv = search->priv; search_advanced_options_setup (search); /* Simulate a new search to update. */ search_entry_activated_cb (GTK_ENTRY (priv->entry), search); } static void search_selection_changed_cb (GtkTreeSelection *selection, DhSearch *search) { DhSearchPriv *priv; GtkTreeIter iter; priv = search->priv; if (gtk_tree_selection_get_selected (selection, NULL, &iter)) { DhLink *link; gtk_tree_model_get (GTK_TREE_MODEL (priv->model), &iter, DH_KEYWORD_MODEL_COL_LINK, &link, -1); if (link != priv->selected_link) { priv->selected_link = link; d(g_print ("Emiting signal with link to: %s (%s)\n", link->name, link->uri)); g_signal_emit (search, signals[LINK_SELECTED], 0, link); } } } /* Make it possible to jump back to the currently selected item, useful when the * html view has been scrolled away. */ static gboolean search_tree_button_press_cb (GtkTreeView *view, GdkEventButton *event, DhSearch *search) { GtkTreePath *path; GtkTreeIter iter; DhSearchPriv *priv; DhLink *link; priv = search->priv; gtk_tree_view_get_path_at_pos (view, event->x, event->y, &path, NULL, NULL, NULL); if (!path) { return FALSE; } gtk_tree_model_get_iter (GTK_TREE_MODEL (priv->model), &iter, path); gtk_tree_path_free (path); gtk_tree_model_get (GTK_TREE_MODEL (priv->model), &iter, DH_KEYWORD_MODEL_COL_LINK, &link, -1); priv->selected_link = link; g_signal_emit (search, signals[LINK_SELECTED], 0, link); /* Always return FALSE so the tree view gets the event and can update * the selection etc. */ return FALSE; } static gboolean search_entry_key_press_event_cb (GtkEntry *entry, GdkEventKey *event, DhSearch *search) { DhSearchPriv *priv; priv = search->priv; if (event->keyval == GDK_Tab) { if (event->state & GDK_CONTROL_MASK) { gtk_widget_grab_focus (priv->hitlist); } else { gtk_editable_set_position (GTK_EDITABLE (entry), -1); gtk_editable_select_region (GTK_EDITABLE (entry), -1, -1); } return TRUE; } if (event->keyval == GDK_Return || event->keyval == GDK_KP_Enter) { GtkTreeIter iter; DhLink *link; gchar *name; /* Get the first entry found. */ if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (priv->model), &iter)) { gtk_tree_model_get (GTK_TREE_MODEL (priv->model), &iter, DH_KEYWORD_MODEL_COL_LINK, &link, DH_KEYWORD_MODEL_COL_NAME, &name, -1); gtk_entry_set_text (GTK_ENTRY (entry), name); g_free (name); gtk_editable_set_position (GTK_EDITABLE (entry), -1); gtk_editable_select_region (GTK_EDITABLE (entry), -1, -1); g_signal_emit (search, signals[LINK_SELECTED], 0, link); return TRUE; } } return FALSE; } static gchar * search_get_search_string (DhSearch *search) { DhSearchPriv *priv; GString *string; priv = search->priv; string = g_string_new (""); if (GTK_WIDGET_VISIBLE (priv->advanced_box) || priv->search_source == SEARCH_API) { if (priv->book_str->len > 0) { g_string_append (string, priv->book_str->str); g_string_append (string, " "); } if (priv->page_str->len > 0) { g_string_append (string, priv->page_str->str); g_string_append (string, " "); } } if (priv->entry_str->len > 0) { g_string_append (string, priv->entry_str->str); g_string_append (string, " "); } return g_string_free (string, FALSE); } static void search_update_string (DhSearch *search, GtkEntry *entry) { const gchar *str = gtk_entry_get_text (entry); DhSearchPriv *priv; priv = search->priv; priv->search_source = SEARCH_ENTRY; if (GTK_WIDGET (entry) == priv->book) { if (str && str[0]) { g_string_printf (priv->book_str, "book:%s", str); } else { g_string_set_size (priv->book_str, 0); } } else if (GTK_WIDGET (entry) == priv->page) { if (str && str[0]) { g_string_printf (priv->page_str, "page:%s", str); } else { g_string_set_size (priv->page_str, 0); } } else { if (GTK_WIDGET_VISIBLE (priv->advanced_box) == FALSE) { g_string_set_size (priv->book_str, 0); g_string_set_size (priv->page_str, 0); } g_string_set_size (priv->entry_str, 0); if (str && str[0]) { g_string_append (priv->entry_str, str); } } } static void search_entry_changed_cb (GtkEntry *entry, DhSearch *search) { DhSearchPriv *priv; priv = search->priv; d(g_print ("Entry changed\n")); search_update_string (search, entry); if (!priv->idle_filter) { priv->idle_filter = g_idle_add ((GSourceFunc) search_filter_idle, search); } } static void search_entry_activated_cb (GtkEntry *entry, DhSearch *search) { DhSearchPriv *priv; DhLink *link; gchar *str; priv = search->priv; str = search_get_search_string (search); link = dh_keyword_model_filter (priv->model, str); g_free (str); } static void search_entry_text_inserted_cb (GtkEntry *entry, const gchar *text, gint length, gint *position, DhSearch *search) { DhSearchPriv *priv; priv = search->priv; if (!priv->idle_complete) { priv->idle_complete = g_idle_add ((GSourceFunc) search_complete_idle, search); } } static gboolean search_complete_idle (DhSearch *search) { DhSearchPriv *priv; const gchar *text; gchar *completed = NULL; GList *list; gint text_length; priv = search->priv; text = gtk_entry_get_text (GTK_ENTRY (priv->entry)); list = g_completion_complete (priv->completion, (gchar *)text, &completed); if (completed) { text_length = strlen (text); gtk_entry_set_text (GTK_ENTRY (priv->entry), completed); gtk_editable_set_position (GTK_EDITABLE (priv->entry), text_length); gtk_editable_select_region (GTK_EDITABLE (priv->entry), text_length, -1); } priv->idle_complete = 0; return FALSE; } static gboolean search_filter_idle (DhSearch *search) { DhSearchPriv *priv; gchar *str; DhLink *link; priv = search->priv; d(g_print ("Filter idle\n")); str = search_get_search_string (search); link = dh_keyword_model_filter (priv->model, str); g_free (str); priv->idle_filter = 0; if (link) { g_signal_emit (search, signals[LINK_SELECTED], 0, link); } return FALSE; } static gchar * search_complete_func (DhLink *link) { return link->name; } static void search_cell_data_func (GtkTreeViewColumn *tree_column, GtkCellRenderer *cell, GtkTreeModel *tree_model, GtkTreeIter *iter, gpointer data) { DhSearch *search; DhSearchPriv *priv; gchar *name; gboolean is_deprecated; GdkColor *color; search = data; priv = search->priv; gtk_tree_model_get (tree_model, iter, DH_KEYWORD_MODEL_COL_NAME, &name, DH_KEYWORD_MODEL_COL_IS_DEPRECATED, &is_deprecated, -1); if (is_deprecated) { color = >K_WIDGET (search)->style->text_aa[GTK_STATE_NORMAL]; } else { color = NULL; } g_object_set (cell, "text", name, "foreground-gdk", color, NULL); g_free (name); } GtkWidget * dh_search_new (GList *keywords) { DhSearch *search; DhSearchPriv *priv; GtkTreeSelection *selection; GtkWidget *list_sw; GtkWidget *frame; GtkWidget *hbox; GtkWidget *book_label, *page_label; GtkSizeGroup *group; GtkCellRenderer *cell; GConfClient *gconf_client; search = g_object_new (DH_TYPE_SEARCH, NULL); priv = search->priv; gtk_container_set_border_width (GTK_CONTAINER (search), 2); /* Setup the book box */ priv->book = gtk_entry_new (); g_signal_connect (priv->book, "changed", G_CALLBACK (search_entry_changed_cb), search); g_signal_connect (priv->book, "activate", G_CALLBACK (search_entry_activated_cb), search); book_label = gtk_label_new_with_mnemonic (_("_Book:")); gtk_label_set_mnemonic_widget (GTK_LABEL (book_label), priv->book); priv->advanced_box = gtk_vbox_new (FALSE, 2); gtk_box_pack_start (GTK_BOX (search), priv->advanced_box, FALSE, FALSE, 0); hbox = gtk_hbox_new (FALSE, 6); gtk_box_pack_start (GTK_BOX (hbox), book_label, FALSE, FALSE, 0); gtk_box_pack_start (GTK_BOX (hbox), priv->book, TRUE, TRUE, 0); gtk_box_pack_start (GTK_BOX (priv->advanced_box), hbox, FALSE, FALSE, 0); /* Setup the page box */ priv->page = gtk_entry_new (); g_signal_connect (priv->page, "changed", G_CALLBACK (search_entry_changed_cb), search); g_signal_connect (priv->page, "activate", G_CALLBACK (search_entry_activated_cb), search); page_label = gtk_label_new_with_mnemonic (_("_Page:")); gtk_label_set_mnemonic_widget (GTK_LABEL (page_label), priv->page); hbox = gtk_hbox_new (FALSE, 6); gtk_box_pack_start (GTK_BOX (hbox), page_label, FALSE, FALSE, 0); gtk_box_pack_start (GTK_BOX (hbox), priv->page, TRUE, TRUE, 0); gtk_box_pack_start (GTK_BOX (priv->advanced_box), hbox, FALSE, FALSE, 0); /* Align the labels */ group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); gtk_size_group_add_widget (group, book_label); gtk_size_group_add_widget (group, page_label); g_object_unref (G_OBJECT (group)); gtk_widget_show_all (priv->advanced_box); gtk_widget_set_no_show_all (priv->advanced_box, TRUE); /* Setup the keyword box */ priv->entry = gtk_entry_new (); g_signal_connect (priv->entry, "key_press_event", G_CALLBACK (search_entry_key_press_event_cb), search); g_signal_connect (priv->hitlist, "button_press_event", G_CALLBACK (search_tree_button_press_cb), search); g_signal_connect (priv->entry, "changed", G_CALLBACK (search_entry_changed_cb), search); g_signal_connect (priv->entry, "activate", G_CALLBACK (search_entry_activated_cb), search); g_signal_connect (priv->entry, "insert_text", G_CALLBACK (search_entry_text_inserted_cb), search); gtk_box_pack_start (GTK_BOX (search), priv->entry, FALSE, FALSE, 0); /* Setup the hitlist */ frame = gtk_frame_new (NULL); gtk_frame_set_shadow_type (GTK_FRAME (frame), GTK_SHADOW_IN); list_sw = gtk_scrolled_window_new (NULL, NULL); gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (list_sw), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); gtk_container_add (GTK_CONTAINER (frame), list_sw); cell = gtk_cell_renderer_text_new (); g_object_set (cell, "ellipsize", PANGO_ELLIPSIZE_END, NULL); gtk_tree_view_insert_column_with_data_func ( GTK_TREE_VIEW (priv->hitlist), -1, NULL, cell, search_cell_data_func, search, NULL); gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (priv->hitlist), FALSE); gtk_tree_view_set_search_column (GTK_TREE_VIEW (priv->hitlist), DH_KEYWORD_MODEL_COL_NAME); selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (priv->hitlist)); g_signal_connect (selection, "changed", G_CALLBACK (search_selection_changed_cb), search); gtk_container_add (GTK_CONTAINER (list_sw), priv->hitlist); gtk_box_pack_end_defaults (GTK_BOX (search), frame); g_completion_add_items (priv->completion, keywords); dh_keyword_model_set_words (priv->model, keywords); gtk_widget_show_all (GTK_WIDGET (search)); gconf_client = dh_base_get_gconf_client (dh_base_get ()); priv->advanced_options_id = gconf_client_notify_add (gconf_client, GCONF_ADVANCED_OPTIONS, search_advanced_options_notify_cb, search, NULL, NULL); search_advanced_options_setup (search); return GTK_WIDGET (search); } void dh_search_set_search_string (DhSearch *search, const gchar *str) { DhSearchPriv *priv; gchar **split, **leftover, *lower; gchar *string = NULL; gint i; g_return_if_fail (DH_IS_SEARCH (search)); priv = search->priv; priv->search_source = SEARCH_API; g_string_set_size (priv->book_str, 0); g_string_set_size (priv->page_str, 0); g_string_set_size (priv->entry_str, 0); g_signal_handlers_block_by_func (priv->book, search_entry_changed_cb, search); g_signal_handlers_block_by_func (priv->page, search_entry_changed_cb, search); g_signal_handlers_block_by_func (priv->entry, search_entry_changed_cb, search); if ((leftover = split = g_strsplit (str, " ", -1)) != NULL) { for (i = 0; split[i] != NULL; i++) { lower = g_ascii_strdown (split[i], -1); /* Determine if there was a book or page specification */ if (!strncmp (lower, "book:", 5)) { g_string_append (priv->book_str, split[i]); leftover++; } else if (!strncmp (lower, "page:", 5)) { g_string_append (priv->page_str, split[i]); leftover++; } else { /* No more specifications */ break; } g_free (lower); } /* Collect the search string */ string = NULL; for (i = 0; leftover[i] != NULL; i++) { if (string == NULL) { string = g_strdup (leftover[i]); } else { lower = g_strdup_printf ("%s %s", string, leftover[i]); g_free (string); string = lower; } } g_strfreev (split); if (string) { g_string_append (priv->entry_str, string); } if (string) { g_free (string); } } else if (str) { g_string_append (priv->entry_str, str); } gtk_entry_set_text (GTK_ENTRY (priv->entry), priv->entry_str->str); if (GTK_WIDGET_VISIBLE (priv->advanced_box)) { gtk_entry_set_text (GTK_ENTRY (priv->book), priv->book_str->len > 5 ? priv->book_str->str + 5 : ""); gtk_entry_set_text (GTK_ENTRY (priv->page), priv->page_str->len > 5 ? priv->page_str->str + 5 : ""); } gtk_editable_set_position (GTK_EDITABLE (priv->entry), -1); gtk_editable_select_region (GTK_EDITABLE (priv->entry), -1, -1); g_signal_handlers_unblock_by_func (priv->book, search_entry_changed_cb, search); g_signal_handlers_unblock_by_func (priv->page, search_entry_changed_cb, search); g_signal_handlers_unblock_by_func (priv->entry, search_entry_changed_cb, search); if (!priv->idle_filter) { priv->idle_filter = g_idle_add ((GSourceFunc) search_filter_idle, search); } } void dh_search_grab_focus (DhSearch *search) { DhSearchPriv *priv; g_return_if_fail (DH_IS_SEARCH (search)); priv = search->priv; gtk_widget_grab_focus (priv->entry); }