Evince
Evince is a document viewer capable of displaying multiple and single page document formats like PDF and Postscript.
 All Data Structures Files Functions Variables Typedefs Enumerations Enumerator Macros Pages
ev-find-sidebar.c
Go to the documentation of this file.
1 /* ev-find-sidebar.c
2  * this file is part of evince, a gnome document viewer
3  *
4  * Copyright (C) 2013 Carlos Garcia Campos <carlosgc@gnome.org>
5  * Copyright (C) 2008 Sergey Pushkin <pushkinsv@gmail.com >
6  *
7  * Evince is free software; you can redistribute it and/or modify it
8  * under the terms of the GNU General Public License as published by
9  * the Free Software Foundation; either version 2 of the License, or
10  * (at your option) any later version.
11  *
12  * Evince is distributed in the hope that it will be useful, but
13  * WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15  * General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program; if not, write to the Free Software
19  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20  */
21 
22 #ifdef HAVE_CONFIG_H
23 #include "config.h"
24 #endif
25 
26 #include "ev-find-sidebar.h"
27 #include <string.h>
28 
30  GtkWidget *tree_view;
31 
32  guint selection_id;
34 
35  GtkTreePath *highlighted_result;
37 
42 };
43 
44 enum {
49 
51 };
52 
53 enum {
56 };
57 
58 static guint signals[N_SIGNALS];
59 
60 G_DEFINE_TYPE (EvFindSidebar, ev_find_sidebar, GTK_TYPE_BOX)
61 
62 static void
64 {
65  EvFindSidebarPrivate *priv = sidebar->priv;
66 
67  if (priv->process_matches_idle_id > 0) {
68  g_source_remove (priv->process_matches_idle_id);
69  priv->process_matches_idle_id = 0;
70  }
71  g_clear_object (&priv->job);
72 }
73 
74 static void
75 ev_find_sidebar_dispose (GObject *object)
76 {
77  EvFindSidebar *sidebar = EV_FIND_SIDEBAR (object);
78 
79  ev_find_sidebar_cancel (sidebar);
80  g_clear_pointer (&sidebar->priv->highlighted_result, (GDestroyNotify)gtk_tree_path_free);
81 
82  G_OBJECT_CLASS (ev_find_sidebar_parent_class)->dispose (object);
83 }
84 
85 static void
87 {
88  GObjectClass *g_object_class = G_OBJECT_CLASS (find_sidebar_class);
89 
90  g_object_class->dispose = ev_find_sidebar_dispose;
91 
93  g_signal_new ("result-activated",
94  G_TYPE_FROM_CLASS (g_object_class),
95  G_SIGNAL_RUN_LAST,
96  0, NULL, NULL,
97  g_cclosure_marshal_generic,
98  G_TYPE_NONE, 2,
99  G_TYPE_INT,
100  G_TYPE_INT);
101 
102  g_type_class_add_private (g_object_class, sizeof (EvFindSidebarPrivate));
103 }
104 
105 static void
107  GtkTreeModel *model,
108  GtkTreeIter *iter)
109 {
110  EvFindSidebarPrivate *priv;
111  gint page;
112  gint result;
113 
114  priv = sidebar->priv;
115 
116  if (priv->highlighted_result)
117  gtk_tree_path_free (priv->highlighted_result);
118  priv->highlighted_result = gtk_tree_model_get_path (model, iter);
119 
120  gtk_tree_model_get (model, iter,
121  PAGE_COLUMN, &page,
122  RESULT_COLUMN, &result,
123  -1);
124  g_signal_emit (sidebar, signals[RESULT_ACTIVATED], 0, page - 1, result);
125 }
126 
127 static void
128 selection_changed_callback (GtkTreeSelection *selection,
129  EvFindSidebar *sidebar)
130 {
131  GtkTreeModel *model;
132  GtkTreeIter iter;
133 
134  if (gtk_tree_selection_get_selected (selection, &model, &iter))
135  ev_find_sidebar_activate_result_at_iter (sidebar, model, &iter);
136 }
137 
138 static gboolean
139 sidebar_tree_button_press_cb (GtkTreeView *view,
140  GdkEventButton *event,
141  EvFindSidebar *sidebar)
142 {
143  EvFindSidebarPrivate *priv;
144  GtkTreeModel *model;
145  GtkTreePath *path;
146  GtkTreeIter iter;
147 
148  priv = sidebar->priv;
149 
150  gtk_tree_view_get_path_at_pos (view, event->x, event->y, &path,
151  NULL, NULL, NULL);
152  if (!path)
153  return FALSE;
154 
155  if (priv->highlighted_result &&
156  gtk_tree_path_compare (priv->highlighted_result, path) != 0) {
157  gtk_tree_path_free (path);
158  return FALSE;
159  }
160 
161  model = gtk_tree_view_get_model (view);
162  gtk_tree_model_get_iter (model, &iter, path);
163  gtk_tree_path_free (path);
164 
165  ev_find_sidebar_activate_result_at_iter (sidebar, model, &iter);
166 
167  /* Always return FALSE so the tree view gets the event and can update
168  * the selection etc.
169  */
170  return FALSE;
171 }
172 
173 static void
175 {
176  GtkListStore *model;
177 
178  model = gtk_list_store_new (N_COLUMNS, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_INT, G_TYPE_INT);
179  gtk_tree_view_set_model (GTK_TREE_VIEW (sidebar->priv->tree_view),
180  GTK_TREE_MODEL (model));
181  g_object_unref (model);
182 }
183 
184 static void
186 {
187  EvFindSidebarPrivate *priv;
188  GtkWidget *swindow;
189  GtkTreeViewColumn *column;
190  GtkCellRenderer *renderer;
191  GtkTreeSelection *selection;
192 
193  sidebar->priv = G_TYPE_INSTANCE_GET_PRIVATE (sidebar, EV_TYPE_FIND_SIDEBAR, EvFindSidebarPrivate);
194  priv = sidebar->priv;
195 
196  swindow = gtk_scrolled_window_new (NULL, NULL);
197  gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (swindow),
198  GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
199 
200  priv->tree_view = gtk_tree_view_new ();
201  ev_find_sidebar_reset_model (sidebar);
202 
203  gtk_tree_view_set_search_column (GTK_TREE_VIEW (priv->tree_view), -1);
204  gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (priv->tree_view), FALSE);
205  gtk_container_add (GTK_CONTAINER (swindow), priv->tree_view);
206  gtk_widget_show (priv->tree_view);
207 
208  gtk_box_pack_start (GTK_BOX (sidebar), swindow, TRUE, TRUE, 0);
209  gtk_widget_show (swindow);
210 
211  column = gtk_tree_view_column_new ();
212  gtk_tree_view_column_set_expand (GTK_TREE_VIEW_COLUMN (column), TRUE);
213  gtk_tree_view_append_column (GTK_TREE_VIEW (priv->tree_view), column);
214 
215  renderer = (GtkCellRenderer *)g_object_new (GTK_TYPE_CELL_RENDERER_TEXT,
216  "ellipsize",
217  PANGO_ELLIPSIZE_END,
218  NULL);
219  gtk_tree_view_column_pack_start (GTK_TREE_VIEW_COLUMN (column), renderer, TRUE);
220  gtk_tree_view_column_set_attributes (GTK_TREE_VIEW_COLUMN (column), renderer,
221  "markup", TEXT_COLUMN,
222  NULL);
223 
224  renderer = gtk_cell_renderer_text_new ();
225  gtk_tree_view_column_pack_end (GTK_TREE_VIEW_COLUMN (column), renderer, FALSE);
226  gtk_tree_view_column_set_attributes (GTK_TREE_VIEW_COLUMN (column), renderer,
227  "text", PAGE_LABEL_COLUMN,
228  NULL);
229  g_object_set (G_OBJECT (renderer), "style", PANGO_STYLE_ITALIC, NULL);
230 
231  selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (priv->tree_view));
232  priv->selection_id = g_signal_connect (selection, "changed",
233  G_CALLBACK (selection_changed_callback),
234  sidebar);
235  g_signal_connect (priv->tree_view, "button-press-event",
236  G_CALLBACK (sidebar_tree_button_press_cb),
237  sidebar);
238 }
239 
240 GtkWidget *
242 {
243  return g_object_new (EV_TYPE_FIND_SIDEBAR,
244  "orientation", GTK_ORIENTATION_VERTICAL,
245  NULL);
246 }
247 
248 static void
250 {
251  EvFindSidebarPrivate *priv = sidebar->priv;
252  GtkTreeSelection *selection;
253 
254  selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (priv->tree_view));
255 
256  g_signal_handler_block (selection, priv->selection_id);
257  gtk_tree_view_set_cursor (GTK_TREE_VIEW (priv->tree_view), priv->highlighted_result, NULL, FALSE);
258  g_signal_handler_unblock (selection, priv->selection_id);
259 }
260 
261 static void
263  gint page)
264 {
265  EvFindSidebarPrivate *priv = sidebar->priv;
266  gint index = 0;
267  gint i;
268 
269  if (!priv->job)
270  return;
271 
272  for (i = 0; i < page; i++)
273  index += ev_job_find_get_n_results (priv->job, i);
274 
275  if (priv->highlighted_result)
276  gtk_tree_path_free (priv->highlighted_result);
277  priv->highlighted_result = gtk_tree_path_new_from_indices (index, -1);
279 }
280 
281 static gchar *
282 sanitized_substring (const gchar *text,
283  gint start,
284  gint end)
285 {
286  const gchar *p;
287  const gchar *start_ptr;
288  const gchar *end_ptr;
289  guint len = 0;
290  gchar *retval;
291 
292  if (end - start <= 0)
293  return NULL;
294 
295  start_ptr = g_utf8_offset_to_pointer (text, start);
296  end_ptr = g_utf8_offset_to_pointer (start_ptr, end - start);
297 
298  retval = g_malloc (end_ptr - start_ptr + 1);
299  p = start_ptr;
300 
301  while (p != end_ptr) {
302  const gchar *next;
303 
304  next = g_utf8_next_char (p);
305 
306  if (next != end_ptr) {
307  GUnicodeBreakType break_type;
308 
309  break_type = g_unichar_break_type (g_utf8_get_char (p));
310  if (break_type == G_UNICODE_BREAK_HYPHEN && *next == '\n') {
311  p = g_utf8_next_char (next);
312  continue;
313  }
314  }
315 
316  if (*p != '\n') {
317  strncpy (retval + len, p, next - p);
318  len += next - p;
319  } else {
320  *(retval + len) = ' ';
321  len++;
322  }
323 
324  p = next;
325  }
326 
327  if (len == 0) {
328  g_free (retval);
329 
330  return NULL;
331  }
332 
333  retval[len] = 0;
334 
335  return retval;
336 }
337 
338 static gchar *
339 get_surrounding_text_markup (const gchar *text,
340  const gchar *find_text,
341  gboolean case_sensitive,
342  PangoLogAttr *log_attrs,
343  gint log_attrs_length,
344  gint offset)
345 {
346  gint iter;
347  gchar *prec = NULL;
348  gchar *succ = NULL;
349  gchar *match = NULL;
350  gchar *markup;
351  gint max_chars;
352 
353  iter = MAX (0, offset - 1);
354  while (!log_attrs[iter].is_word_start && iter > 0)
355  iter--;
356 
357  prec = sanitized_substring (text, iter, offset);
358 
359  iter = offset;
360  offset += g_utf8_strlen (find_text, -1);
361  if (!case_sensitive)
362  match = g_utf8_substring (text, iter, offset);
363 
364  iter = MIN (log_attrs_length, offset + 1);
365  max_chars = MIN (log_attrs_length - 1, iter + 100);
366  while (TRUE) {
367  gint word = iter;
368 
369  while (!log_attrs[word].is_word_end && word < max_chars)
370  word++;
371 
372  if (word > max_chars)
373  break;
374 
375  iter = word + 1;
376  }
377 
378  succ = sanitized_substring (text, offset, iter);
379 
380  markup = g_markup_printf_escaped ("%s<span weight=\"bold\">%s</span>%s",
381  prec ? prec : "", match ? match : find_text, succ ? succ : "");
382  g_free (prec);
383  g_free (succ);
384  g_free (match);
385 
386  return markup;
387 }
388 
389 static gchar *
391  EvPage *page,
392  EvRectangle **areas,
393  guint *n_areas)
394 {
395  gchar *text;
396  gboolean success;
397 
399  text = ev_document_text_get_text (EV_DOCUMENT_TEXT (document), page);
400  success = ev_document_text_get_text_layout (EV_DOCUMENT_TEXT (document), page, areas, n_areas);
402 
403  if (!success) {
404  g_free (text);
405  return NULL;
406  }
407 
408  return text;
409 }
410 
411 static gint
413  guint n_areas,
414  EvRectangle *match,
415  gint offset)
416 {
417  gdouble x, y;
418  gint i;
419 
420  x = match->x1;
421  y = (match->y1 + match->y2) / 2;
422 
423  i = offset;
424 
425  do {
426  EvRectangle *area = areas + i;
427 
428  if (x >= area->x1 && x < area->x2 &&
429  y >= area->y1 && y <= area->y2) {
430  return i;
431  }
432 
433  i = (i + 1) % n_areas;
434  } while (i != offset);
435 
436  return -1;
437 }
438 
439 static gboolean
441 {
442  EvFindSidebarPrivate *priv = sidebar->priv;
443  GtkTreeModel *model;
444  gint current_page;
445  EvDocument *document;
446 
447  priv->process_matches_idle_id = 0;
448 
449  if (!ev_job_find_has_results (priv->job)) {
450  if (ev_job_is_finished (EV_JOB (priv->job)))
451  g_clear_object (&priv->job);
452  return FALSE;
453  }
454 
455  document = EV_JOB (priv->job)->document;
456  model = gtk_tree_view_get_model (GTK_TREE_VIEW (priv->tree_view));
457 
458  do {
459  GList *matches, *l;
460  EvPage *page;
461  gint result;
462  gchar *page_label;
463  gchar *page_text;
464  EvRectangle *areas = NULL;
465  guint n_areas;
466  PangoLogAttr *text_log_attrs;
467  gulong text_log_attrs_length;
468  gint offset;
469 
470  current_page = priv->current_page;
471  priv->current_page = (priv->current_page + 1) % priv->job->n_pages;
472 
473  matches = priv->job->pages[current_page];
474  if (!matches)
475  continue;
476 
477  page = ev_document_get_page (document, current_page);
478  page_label = ev_document_get_page_label (document, current_page);
479  page_text = get_page_text (document, page, &areas, &n_areas);
480  g_object_unref (page);
481  if (!page_text)
482  continue;
483 
484  text_log_attrs_length = g_utf8_strlen (page_text, -1);
485  text_log_attrs = g_new0 (PangoLogAttr, text_log_attrs_length + 1);
486  pango_get_log_attrs (page_text, -1, -1, NULL, text_log_attrs, text_log_attrs_length + 1);
487 
488  if (priv->first_match_page == -1)
489  priv->first_match_page = current_page;
490 
491  offset = 0;
492 
493  for (l = matches, result = 0; l; l = g_list_next (l), result++) {
494  EvRectangle *match = (EvRectangle *)l->data;
495  gchar *markup;
496  GtkTreeIter iter;
497 
498  offset = get_match_offset (areas, n_areas, match, offset);
499  if (offset == -1) {
500  g_warning ("No offset found for match \"%s\" at page %d after processing %d results\n",
501  priv->job->text, current_page, result);
502  break;
503  }
504 
505  if (current_page >= priv->job->start_page) {
506  gtk_list_store_append (GTK_LIST_STORE (model), &iter);
507  } else {
508  gtk_list_store_insert (GTK_LIST_STORE (model), &iter,
509  priv->insert_position);
510  priv->insert_position++;
511  }
512 
513  markup = get_surrounding_text_markup (page_text,
514  priv->job->text,
515  priv->job->case_sensitive,
516  text_log_attrs,
517  text_log_attrs_length,
518  offset);
519 
520  gtk_list_store_set (GTK_LIST_STORE (model), &iter,
521  TEXT_COLUMN, markup,
522  PAGE_LABEL_COLUMN, page_label,
523  PAGE_COLUMN, current_page + 1,
524  RESULT_COLUMN, result,
525  -1);
526  g_free (markup);
527  }
528 
529  g_free (page_label);
530  g_free (page_text);
531  g_free (text_log_attrs);
532  g_free (areas);
533  } while (current_page != priv->job_current_page);
534 
535  if (ev_job_is_finished (EV_JOB (priv->job)) && priv->current_page == priv->job->start_page)
537 
538  return FALSE;
539 }
540 
541 static void
543  gint page,
544  EvFindSidebar *sidebar)
545 {
546  sidebar->priv->job_current_page = page;
547 }
548 
549 static void
551  EvFindSidebar *sidebar)
552 {
553  ev_find_sidebar_cancel (sidebar);
554 }
555 
556 void
558  EvJobFind *job)
559 {
560  EvFindSidebarPrivate *priv = sidebar->priv;
561 
562  if (priv->job == job)
563  return;
564 
565  ev_find_sidebar_clear (sidebar);
566  priv->job = g_object_ref (job);
567  g_signal_connect_object (job, "updated",
568  G_CALLBACK (find_job_updated_cb),
569  sidebar, 0);
570  g_signal_connect_object (job, "cancelled",
571  G_CALLBACK (find_job_cancelled_cb),
572  sidebar, 0);
573  priv->job_current_page = -1;
574  priv->first_match_page = -1;
575  priv->current_page = job->start_page;
576  priv->insert_position = 0;
577 }
578 
579 void
581  gint page)
582 {
583  EvFindSidebarPrivate *priv = sidebar->priv;
584  gint first_match_page = -1;
585  gint i;
586 
587  if (!priv->job)
588  return;
589 
590  for (i = 0; i < priv->job->n_pages; i++) {
591  int index;
592 
593  index = page + i;
594 
595  if (index >= priv->job->n_pages)
596  index -= priv->job->n_pages;
597 
598  if (priv->job->pages[index]) {
599  first_match_page = index;
600  break;
601  }
602  }
603 
604  if (first_match_page != -1)
605  ev_find_sidebar_highlight_first_match_of_page (sidebar, first_match_page);
606 }
607 
608 void
610 {
611  EvFindSidebarPrivate *priv = sidebar->priv;
612 
613  if (!priv->job)
614  return;
615 
616  if (priv->process_matches_idle_id == 0)
617  priv->process_matches_idle_id = g_idle_add ((GSourceFunc)process_matches_idle, sidebar);
618 }
619 
620 void
622 {
623  EvFindSidebarPrivate *priv = sidebar->priv;
624 
625  ev_find_sidebar_cancel (sidebar);
626 
627  /* It seems it's more efficient to set a new model in the tree view instead of
628  * clearing the model that would emit row-deleted signal for every row in the model
629  */
630  ev_find_sidebar_reset_model (sidebar);
631  g_clear_pointer (&priv->highlighted_result, (GDestroyNotify)gtk_tree_path_free);
632 }
633 
634 void
636 {
637  EvFindSidebarPrivate *priv = sidebar->priv;
638 
639  if (!priv->highlighted_result)
640  return;
641 
642  if (!gtk_tree_path_prev (priv->highlighted_result)) {
643  GtkTreeModel *model;
644  GtkTreeIter iter;
645 
646  model = gtk_tree_view_get_model (GTK_TREE_VIEW (priv->tree_view));
647  gtk_tree_model_get_iter (model, &iter, priv->highlighted_result);
648  while (gtk_tree_model_iter_next (model, &iter))
649  gtk_tree_path_next (priv->highlighted_result);
650  }
652 }
653 
654 void
656 {
657  EvFindSidebarPrivate *priv = sidebar->priv;
658  GtkTreeModel *model;
659  GtkTreeIter iter;
660 
661  if (!priv->highlighted_result)
662  return;
663 
664  model = gtk_tree_view_get_model (GTK_TREE_VIEW (priv->tree_view));
665  gtk_tree_model_get_iter (model, &iter, priv->highlighted_result);
666  if (gtk_tree_model_iter_next (model, &iter)) {
667  gtk_tree_path_next (priv->highlighted_result);
668  } else {
669  gtk_tree_path_free (priv->highlighted_result);
670  priv->highlighted_result = gtk_tree_path_new_first ();
671  }
673 }