summaryrefslogtreecommitdiff
path: root/shell/ev-find-sidebar.c
diff options
context:
space:
mode:
Diffstat (limited to 'shell/ev-find-sidebar.c')
-rw-r--r--shell/ev-find-sidebar.c681
1 files changed, 681 insertions, 0 deletions
diff --git a/shell/ev-find-sidebar.c b/shell/ev-find-sidebar.c
new file mode 100644
index 00000000..30718ec7
--- /dev/null
+++ b/shell/ev-find-sidebar.c
@@ -0,0 +1,681 @@
+/* ev-find-sidebar.c
+ * this file is part of evince, a gnome document viewer
+ *
+ * Copyright (C) 2013 Carlos Garcia Campos <[email protected]>
+ * Copyright (C) 2008 Sergey Pushkin <[email protected] >
+ *
+ * Evince 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.
+ *
+ * Evince 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "ev-find-sidebar.h"
+
+#include "ev-document-text.h"
+
+#include <string.h>
+
+typedef struct {
+ GtkWidget *tree_view;
+
+ guint selection_id;
+ guint process_matches_idle_id;
+
+ GtkTreePath *highlighted_result;
+ gint first_match_page;
+
+ EvJobFind *job;
+ gint job_current_page;
+ gint current_page;
+ gint insert_position;
+} EvFindSidebarPrivate;
+
+enum {
+ TEXT_COLUMN,
+ PAGE_LABEL_COLUMN,
+ PAGE_COLUMN,
+ RESULT_COLUMN,
+
+ N_COLUMNS
+};
+
+enum {
+ RESULT_ACTIVATED,
+ N_SIGNALS
+};
+
+static guint signals[N_SIGNALS];
+
+G_DEFINE_TYPE_WITH_PRIVATE (EvFindSidebar, ev_find_sidebar, GTK_TYPE_BOX)
+
+#define GET_PRIVATE(o) ev_find_sidebar_get_instance_private (o)
+
+static void
+ev_find_sidebar_cancel (EvFindSidebar *sidebar)
+{
+ EvFindSidebarPrivate *priv = GET_PRIVATE (sidebar);
+
+ if (priv->process_matches_idle_id > 0) {
+ g_source_remove (priv->process_matches_idle_id);
+ priv->process_matches_idle_id = 0;
+ }
+ g_clear_object (&priv->job);
+}
+
+static void
+ev_find_sidebar_dispose (GObject *object)
+{
+ EvFindSidebar *sidebar = EV_FIND_SIDEBAR (object);
+ EvFindSidebarPrivate *priv = GET_PRIVATE (sidebar);
+
+ ev_find_sidebar_cancel (sidebar);
+ g_clear_pointer (&(priv->highlighted_result), gtk_tree_path_free);
+
+ G_OBJECT_CLASS (ev_find_sidebar_parent_class)->dispose (object);
+}
+
+static void
+ev_find_sidebar_class_init (EvFindSidebarClass *find_sidebar_class)
+{
+ GObjectClass *g_object_class = G_OBJECT_CLASS (find_sidebar_class);
+
+ g_object_class->dispose = ev_find_sidebar_dispose;
+
+ signals[RESULT_ACTIVATED] =
+ g_signal_new ("result-activated",
+ G_TYPE_FROM_CLASS (g_object_class),
+ G_SIGNAL_RUN_LAST,
+ 0, NULL, NULL,
+ g_cclosure_marshal_generic,
+ G_TYPE_NONE, 2,
+ G_TYPE_INT,
+ G_TYPE_INT);
+}
+
+static void
+ev_find_sidebar_activate_result_at_iter (EvFindSidebar *sidebar,
+ GtkTreeModel *model,
+ GtkTreeIter *iter)
+{
+ EvFindSidebarPrivate *priv = GET_PRIVATE (sidebar);
+ gint page;
+ gint result;
+
+
+ if (priv->highlighted_result)
+ gtk_tree_path_free (priv->highlighted_result);
+ priv->highlighted_result = gtk_tree_model_get_path (model, iter);
+
+ gtk_tree_model_get (model, iter,
+ PAGE_COLUMN, &page,
+ RESULT_COLUMN, &result,
+ -1);
+ g_signal_emit (sidebar, signals[RESULT_ACTIVATED], 0, page - 1, result);
+}
+
+static void
+selection_changed_callback (GtkTreeSelection *selection,
+ EvFindSidebar *sidebar)
+{
+ GtkTreeModel *model;
+ GtkTreeIter iter;
+
+ if (gtk_tree_selection_get_selected (selection, &model, &iter))
+ ev_find_sidebar_activate_result_at_iter (sidebar, model, &iter);
+}
+
+static gboolean
+sidebar_tree_button_press_cb (GtkTreeView *view,
+ GdkEventButton *event,
+ EvFindSidebar *sidebar)
+{
+ EvFindSidebarPrivate *priv = GET_PRIVATE (sidebar);
+ GtkTreeModel *model;
+ GtkTreePath *path;
+ GtkTreeIter iter;
+
+ gtk_tree_view_get_path_at_pos (view, event->x, event->y, &path,
+ NULL, NULL, NULL);
+ if (!path)
+ return FALSE;
+
+ if (priv->highlighted_result &&
+ gtk_tree_path_compare (priv->highlighted_result, path) != 0) {
+ gtk_tree_path_free (path);
+ return FALSE;
+ }
+
+ model = gtk_tree_view_get_model (view);
+ gtk_tree_model_get_iter (model, &iter, path);
+ gtk_tree_path_free (path);
+
+ ev_find_sidebar_activate_result_at_iter (sidebar, model, &iter);
+
+ /* Always return FALSE so the tree view gets the event and can update
+ * the selection etc.
+ */
+ return FALSE;
+}
+
+static void
+ev_find_sidebar_reset_model (EvFindSidebar *sidebar)
+{
+ EvFindSidebarPrivate *priv = GET_PRIVATE (sidebar);
+ GtkListStore *model;
+
+ model = gtk_list_store_new (N_COLUMNS, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_INT, G_TYPE_INT);
+ gtk_tree_view_set_model (GTK_TREE_VIEW (priv->tree_view),
+ GTK_TREE_MODEL (model));
+ g_object_unref (model);
+}
+
+static void
+ev_find_sidebar_init (EvFindSidebar *sidebar)
+{
+ EvFindSidebarPrivate *priv;
+ GtkWidget *swindow;
+ GtkTreeViewColumn *column;
+ GtkCellRenderer *renderer;
+ GtkTreeSelection *selection;
+
+ priv = GET_PRIVATE (sidebar);
+
+ swindow = gtk_scrolled_window_new (NULL, NULL);
+ gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (swindow),
+ GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
+
+ priv->tree_view = gtk_tree_view_new ();
+ ev_find_sidebar_reset_model (sidebar);
+
+ gtk_tree_view_set_search_column (GTK_TREE_VIEW (priv->tree_view), -1);
+ gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (priv->tree_view), FALSE);
+ gtk_container_add (GTK_CONTAINER (swindow), priv->tree_view);
+ gtk_widget_show (priv->tree_view);
+
+ gtk_box_pack_start (GTK_BOX (sidebar), swindow, TRUE, TRUE, 0);
+ gtk_widget_show (swindow);
+
+ column = gtk_tree_view_column_new ();
+ gtk_tree_view_column_set_expand (GTK_TREE_VIEW_COLUMN (column), TRUE);
+ gtk_tree_view_append_column (GTK_TREE_VIEW (priv->tree_view), column);
+
+ renderer = (GtkCellRenderer *)g_object_new (GTK_TYPE_CELL_RENDERER_TEXT,
+ "ellipsize",
+ PANGO_ELLIPSIZE_END,
+ NULL);
+ gtk_tree_view_column_pack_start (GTK_TREE_VIEW_COLUMN (column), renderer, TRUE);
+ gtk_tree_view_column_set_attributes (GTK_TREE_VIEW_COLUMN (column), renderer,
+ "markup", TEXT_COLUMN,
+ NULL);
+
+ renderer = gtk_cell_renderer_text_new ();
+ gtk_tree_view_column_pack_end (GTK_TREE_VIEW_COLUMN (column), renderer, FALSE);
+ gtk_tree_view_column_set_attributes (GTK_TREE_VIEW_COLUMN (column), renderer,
+ "text", PAGE_LABEL_COLUMN,
+ NULL);
+ g_object_set (G_OBJECT (renderer), "style", PANGO_STYLE_ITALIC, NULL);
+
+ selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (priv->tree_view));
+ priv->selection_id = g_signal_connect (selection, "changed",
+ G_CALLBACK (selection_changed_callback),
+ sidebar);
+ g_signal_connect (priv->tree_view, "button-press-event",
+ G_CALLBACK (sidebar_tree_button_press_cb),
+ sidebar);
+}
+
+GtkWidget *
+ev_find_sidebar_new (void)
+{
+ return g_object_new (EV_TYPE_FIND_SIDEBAR,
+ "orientation", GTK_ORIENTATION_VERTICAL,
+ NULL);
+}
+
+static void
+ev_find_sidebar_select_highlighted_result (EvFindSidebar *sidebar)
+{
+ EvFindSidebarPrivate *priv = GET_PRIVATE (sidebar);
+ GtkTreeSelection *selection;
+
+ selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (priv->tree_view));
+
+ g_signal_handler_block (selection, priv->selection_id);
+ gtk_tree_view_set_cursor (GTK_TREE_VIEW (priv->tree_view), priv->highlighted_result, NULL, FALSE);
+ g_signal_handler_unblock (selection, priv->selection_id);
+}
+
+static void
+ev_find_sidebar_highlight_first_match_of_page (EvFindSidebar *sidebar,
+ gint page)
+{
+ EvFindSidebarPrivate *priv = GET_PRIVATE (sidebar);
+ gint index = 0;
+ gint i;
+
+ if (!priv->job)
+ return;
+
+ for (i = 0; i < page; i++)
+ index += ev_job_find_get_n_results (priv->job, i);
+
+ if (priv->highlighted_result)
+ gtk_tree_path_free (priv->highlighted_result);
+ priv->highlighted_result = gtk_tree_path_new_from_indices (index, -1);
+ ev_find_sidebar_select_highlighted_result (sidebar);
+}
+
+static gchar *
+sanitized_substring (const gchar *text,
+ gint start,
+ gint end)
+{
+ const gchar *p;
+ const gchar *start_ptr;
+ const gchar *end_ptr;
+ guint len = 0;
+ gchar *retval;
+
+ if (end - start <= 0)
+ return NULL;
+
+ start_ptr = g_utf8_offset_to_pointer (text, start);
+ end_ptr = g_utf8_offset_to_pointer (start_ptr, end - start);
+
+ retval = g_malloc (end_ptr - start_ptr + 1);
+ p = start_ptr;
+
+ while (p != end_ptr) {
+ const gchar *next;
+
+ next = g_utf8_next_char (p);
+
+ if (next != end_ptr) {
+ GUnicodeBreakType break_type;
+
+ break_type = g_unichar_break_type (g_utf8_get_char (p));
+ if (break_type == G_UNICODE_BREAK_HYPHEN && *next == '\n') {
+ p = g_utf8_next_char (next);
+ continue;
+ }
+ }
+
+ if (*p != '\n') {
+ strncpy (retval + len, p, next - p);
+ len += next - p;
+ } else {
+ *(retval + len) = ' ';
+ len++;
+ }
+
+ p = next;
+ }
+
+ if (len == 0) {
+ g_free (retval);
+
+ return NULL;
+ }
+
+ retval[len] = 0;
+
+ return retval;
+}
+
+static gchar *
+get_surrounding_text_markup (const gchar *text,
+ const gchar *find_text,
+ gboolean case_sensitive,
+ PangoLogAttr *log_attrs,
+ gint log_attrs_length,
+ gint offset)
+{
+ gint iter;
+ gchar *prec = NULL;
+ gchar *succ = NULL;
+ gchar *match = NULL;
+ gchar *markup;
+ gint max_chars;
+
+ iter = MAX (0, offset - 1);
+ while (!log_attrs[iter].is_word_start && iter > 0)
+ iter--;
+
+ prec = sanitized_substring (text, iter, offset);
+
+ iter = offset;
+ offset += g_utf8_strlen (find_text, -1);
+ if (!case_sensitive)
+ match = g_utf8_substring (text, iter, offset);
+
+ iter = MIN (log_attrs_length, offset + 1);
+ max_chars = MIN (log_attrs_length - 1, iter + 100);
+ while (TRUE) {
+ gint word = iter;
+
+ while (!log_attrs[word].is_word_end && word < max_chars)
+ word++;
+
+ if (word > max_chars)
+ break;
+
+ iter = word + 1;
+ }
+
+ succ = sanitized_substring (text, offset, iter);
+
+ markup = g_markup_printf_escaped ("%s<span weight=\"bold\">%s</span>%s",
+ prec ? prec : "", match ? match : find_text, succ ? succ : "");
+ g_free (prec);
+ g_free (succ);
+ g_free (match);
+
+ return markup;
+}
+
+static gchar *
+get_page_text (EvDocument *document,
+ EvPage *page,
+ EvRectangle **areas,
+ guint *n_areas)
+{
+ gchar *text;
+ gboolean success;
+
+ ev_document_doc_mutex_lock ();
+ text = ev_document_text_get_text (EV_DOCUMENT_TEXT (document), page);
+ success = ev_document_text_get_text_layout (EV_DOCUMENT_TEXT (document), page, areas, n_areas);
+ ev_document_doc_mutex_unlock ();
+
+ if (!success) {
+ g_free (text);
+ return NULL;
+ }
+
+ return text;
+}
+
+static gint
+get_match_offset (EvRectangle *areas,
+ guint n_areas,
+ EvRectangle *match,
+ gint offset)
+{
+ gdouble x, y;
+ gint i;
+
+ x = match->x1;
+ y = (match->y1 + match->y2) / 2;
+
+ i = offset;
+
+ do {
+ EvRectangle *area = areas + i;
+
+ if (x >= area->x1 && x < area->x2 &&
+ y >= area->y1 && y <= area->y2) {
+ return i;
+ }
+
+ i = (i + 1) % n_areas;
+ } while (i != offset);
+
+ return -1;
+}
+
+static gboolean
+process_matches_idle (EvFindSidebar *sidebar)
+{
+ EvFindSidebarPrivate *priv = GET_PRIVATE (sidebar);
+ GtkTreeModel *model;
+ gint current_page;
+ EvDocument *document;
+
+ priv->process_matches_idle_id = 0;
+
+ if (!ev_job_find_has_results (priv->job)) {
+ if (ev_job_is_finished (EV_JOB (priv->job)))
+ g_clear_object (&priv->job);
+ return FALSE;
+ }
+
+ document = EV_JOB (priv->job)->document;
+ model = gtk_tree_view_get_model (GTK_TREE_VIEW (priv->tree_view));
+
+ do {
+ GList *matches, *l;
+ EvPage *page;
+ gint result;
+ gchar *page_label;
+ gchar *page_text;
+ EvRectangle *areas = NULL;
+ guint n_areas;
+ PangoLogAttr *text_log_attrs;
+ gulong text_log_attrs_length;
+ gint offset;
+
+ current_page = priv->current_page;
+ priv->current_page = (priv->current_page + 1) % priv->job->n_pages;
+
+ matches = priv->job->pages[current_page];
+ if (!matches)
+ continue;
+
+ page = ev_document_get_page (document, current_page);
+ page_label = ev_document_get_page_label (document, current_page);
+ page_text = get_page_text (document, page, &areas, &n_areas);
+ g_object_unref (page);
+ if (!page_text)
+ continue;
+
+ text_log_attrs_length = g_utf8_strlen (page_text, -1);
+ text_log_attrs = g_new0 (PangoLogAttr, text_log_attrs_length + 1);
+ pango_get_log_attrs (page_text, -1, -1, NULL, text_log_attrs, text_log_attrs_length + 1);
+
+ if (priv->first_match_page == -1)
+ priv->first_match_page = current_page;
+
+ offset = 0;
+
+ for (l = matches, result = 0; l; l = g_list_next (l), result++) {
+ EvRectangle *match = (EvRectangle *)l->data;
+ gchar *markup;
+ GtkTreeIter iter;
+ gint new_offset;
+
+ new_offset = get_match_offset (areas, n_areas, match, offset);
+ if (new_offset == -1) {
+ g_warning ("No offset found for match \"%s\" at page %d after processing %d results\n",
+ priv->job->text, current_page, result);
+ /* It may happen that a vertical text match has no corresponding text area, skip
+ * that but keep iterating to show any other matches in page (issue #1545) */
+ continue;
+ }
+ offset = new_offset;
+
+ if (current_page >= priv->job->start_page) {
+ gtk_list_store_append (GTK_LIST_STORE (model), &iter);
+ } else {
+ gtk_list_store_insert (GTK_LIST_STORE (model), &iter,
+ priv->insert_position);
+ priv->insert_position++;
+ }
+
+ markup = get_surrounding_text_markup (page_text,
+ priv->job->text,
+ priv->job->case_sensitive,
+ text_log_attrs,
+ text_log_attrs_length,
+ offset);
+
+ gtk_list_store_set (GTK_LIST_STORE (model), &iter,
+ TEXT_COLUMN, markup,
+ PAGE_LABEL_COLUMN, page_label,
+ PAGE_COLUMN, current_page + 1,
+ RESULT_COLUMN, result,
+ -1);
+ g_free (markup);
+ }
+
+ g_free (page_label);
+ g_free (page_text);
+ g_free (text_log_attrs);
+ g_free (areas);
+ } while (current_page != priv->job_current_page);
+
+ if (ev_job_is_finished (EV_JOB (priv->job)) && priv->current_page == priv->job->start_page)
+ ev_find_sidebar_highlight_first_match_of_page (sidebar, priv->first_match_page);
+
+ return FALSE;
+}
+
+static void
+find_job_updated_cb (EvJobFind *job,
+ gint page,
+ EvFindSidebar *sidebar)
+{
+ EvFindSidebarPrivate *priv = GET_PRIVATE (sidebar);
+
+ priv->job_current_page = page;
+}
+
+static void
+find_job_cancelled_cb (EvJob *job,
+ EvFindSidebar *sidebar)
+{
+ ev_find_sidebar_cancel (sidebar);
+}
+
+void
+ev_find_sidebar_start (EvFindSidebar *sidebar,
+ EvJobFind *job)
+{
+ EvFindSidebarPrivate *priv = GET_PRIVATE (sidebar);
+
+ if (priv->job == job)
+ return;
+
+ ev_find_sidebar_clear (sidebar);
+ priv->job = g_object_ref (job);
+ g_signal_connect_object (job, "updated",
+ G_CALLBACK (find_job_updated_cb),
+ sidebar, 0);
+ g_signal_connect_object (job, "cancelled",
+ G_CALLBACK (find_job_cancelled_cb),
+ sidebar, 0);
+ priv->job_current_page = -1;
+ priv->first_match_page = -1;
+ priv->current_page = job->start_page;
+ priv->insert_position = 0;
+}
+
+void
+ev_find_sidebar_restart (EvFindSidebar *sidebar,
+ gint page)
+{
+ EvFindSidebarPrivate *priv = GET_PRIVATE (sidebar);
+ gint first_match_page = -1;
+ gint i;
+
+ if (!priv->job)
+ return;
+
+ for (i = 0; i < priv->job->n_pages; i++) {
+ int index;
+
+ index = page + i;
+
+ if (index >= priv->job->n_pages)
+ index -= priv->job->n_pages;
+
+ if (priv->job->pages[index]) {
+ first_match_page = index;
+ break;
+ }
+ }
+
+ if (first_match_page != -1)
+ ev_find_sidebar_highlight_first_match_of_page (sidebar, first_match_page);
+}
+
+void
+ev_find_sidebar_update (EvFindSidebar *sidebar)
+{
+ EvFindSidebarPrivate *priv = GET_PRIVATE (sidebar);
+
+ if (!priv->job)
+ return;
+
+ if (priv->process_matches_idle_id == 0)
+ priv->process_matches_idle_id = g_idle_add ((GSourceFunc)process_matches_idle, sidebar);
+}
+
+void
+ev_find_sidebar_clear (EvFindSidebar *sidebar)
+{
+ EvFindSidebarPrivate *priv = GET_PRIVATE (sidebar);
+
+ ev_find_sidebar_cancel (sidebar);
+
+ /* It seems it's more efficient to set a new model in the tree view instead of
+ * clearing the model that would emit row-deleted signal for every row in the model
+ */
+ ev_find_sidebar_reset_model (sidebar);
+ g_clear_pointer (&priv->highlighted_result, gtk_tree_path_free);
+}
+
+void
+ev_find_sidebar_previous (EvFindSidebar *sidebar)
+{
+ EvFindSidebarPrivate *priv = GET_PRIVATE (sidebar);
+
+ if (!priv->highlighted_result)
+ return;
+
+ if (!gtk_tree_path_prev (priv->highlighted_result)) {
+ GtkTreeModel *model;
+ GtkTreeIter iter;
+
+ model = gtk_tree_view_get_model (GTK_TREE_VIEW (priv->tree_view));
+ gtk_tree_model_get_iter (model, &iter, priv->highlighted_result);
+ while (gtk_tree_model_iter_next (model, &iter))
+ gtk_tree_path_next (priv->highlighted_result);
+ }
+ ev_find_sidebar_select_highlighted_result (sidebar);
+}
+
+void
+ev_find_sidebar_next (EvFindSidebar *sidebar)
+{
+ EvFindSidebarPrivate *priv = GET_PRIVATE (sidebar);
+ GtkTreeModel *model;
+ GtkTreeIter iter;
+
+ if (!priv->highlighted_result)
+ return;
+
+ model = gtk_tree_view_get_model (GTK_TREE_VIEW (priv->tree_view));
+ gtk_tree_model_get_iter (model, &iter, priv->highlighted_result);
+ if (gtk_tree_model_iter_next (model, &iter)) {
+ gtk_tree_path_next (priv->highlighted_result);
+ } else {
+ gtk_tree_path_free (priv->highlighted_result);
+ priv->highlighted_result = gtk_tree_path_new_first ();
+ }
+ ev_find_sidebar_select_highlighted_result (sidebar);
+}
+