4. Searching

In this section, you will learn how to search through a text buffer. Along the way you will learn about marks, as well. We will start of with implementing a basic search, and then add more features to it.

4.1. Simple Search

The following functions can be used to search for a given text within a buffer.


gboolean gtk_text_iter_forward_search( const GtkTextIter *iter,
                                       const gchar *str,
                                       GtkTextSearchFlags flags,
                                       GtkTextIter *match_start,
                                       GtkTextIter *match_end,
                                       const GtkTextIter *limit );

gboolean gtk_text_iter_backward_search( const GtkTextIter *iter,
                                        const gchar *str,
                                        GtkTextSearchFlags flags,
                                        GtkTextIter *match_start,
                                        GtkTextIter *match_end,
                                        const GtkTextIter *limit );
The function gtk_text_iter_forward_search searches for str starting from iter in the forward direction. If match_start and match_end are not NULL, the start and end iters of the first matched string are stored in them. The search is limited to the iter limit, if specified. The function returns TRUE, if a match is found. The function gtk_text_iter_backward_search is same as gtk_text_iter_forward_search but, as its name suggests, it searches in the backward direction.

The function gtk_buffer_selection_bounds was introduced earlier, to obtain the iters around the current selection. To set the current selection programmatically the following function can be used.


void gtk_text_buffer_select_range( GtkTextBuffer *buffer,
                                   const GtkTextIter *start,
                                   const GtkTextIter *end );
The function sets the selection bounds of buffer to start and end. The following example which demonstrates searching, uses this function to highlight matched text.


#include <gtk/gtk.h>

typedef struct _App 
{
  GtkWidget *text_view;
  GtkWidget *search_entry;
} App;

/* Called when main window is destroyed. */
void
win_destroy (void)
{
  gtk_main_quit();
}

/* Called when search button is clicked. */
void
search_button_clicked (GtkWidget *search_button, App *app)
{
  const gchar *text;
  GtkTextBuffer *buffer;
  GtkTextIter iter;
  GtkTextIter mstart, mend; 
  gboolean found;

  text = gtk_entry_get_text (GTK_ENTRY (app->search_entry));

  /* Get the buffer associated with the text view widget. */
  buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (app->text_view));
  /* Search from the start from buffer for text. */
  gtk_text_buffer_get_start_iter (buffer, &iter);
  found = gtk_text_iter_forward_search (&iter, text, 0, &mstart, &mend, NULL);

  if (found)
    {
      /* If found, hilight the text. */
      gtk_text_buffer_select_range (buffer, &mstart, &mend);
    }
}

int 
main (int argc, char *argv[])
{
  GtkWidget *win;
  GtkWidget *vbox;
  GtkWidget *hbox;
  GtkWidget *search_button;
  GtkWidget *swindow;

  App app;

  gtk_init (&argc, &argv);

  /* Create a window with a search entry, search button and a text
     area. */
  win = gtk_window_new (GTK_WINDOW_TOPLEVEL);
  g_signal_connect (G_OBJECT (win), "destroy", win_destroy, NULL);

  vbox = gtk_vbox_new (FALSE, 2);
  gtk_container_add (GTK_CONTAINER (win), vbox);

  hbox = gtk_hbox_new (FALSE, 2);
  gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, FALSE, 0);
  
  app.search_entry = gtk_entry_new ();
  gtk_box_pack_start (GTK_BOX (hbox), app.search_entry, TRUE, TRUE, 0);

  search_button = gtk_button_new_with_label ("Search");  
  gtk_box_pack_start (GTK_BOX (hbox), search_button, FALSE, FALSE, 0);
  g_signal_connect (G_OBJECT (search_button), "clicked", 
                    G_CALLBACK (search_button_clicked), &app);

  /* A scrolled window which automatically displays horizontal and
     vertical scrollbars when the text exceeds the text view's size. */
  swindow = gtk_scrolled_window_new (NULL, NULL);
  gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (swindow),
                                  GTK_POLICY_AUTOMATIC,
                                  GTK_POLICY_AUTOMATIC);
  gtk_box_pack_start (GTK_BOX (vbox), swindow, TRUE, TRUE, 0);

  app.text_view = gtk_text_view_new ();
  gtk_container_add (GTK_CONTAINER (swindow), app.text_view);

  gtk_widget_show_all (win);

  gtk_main();
}

4.2. Continuing your Search

If you had executed the above program you would have noted that, if there were more than one occurrence of the text in the buffer, pressing search will only highlight the first occurrence of the text. To provide a feature similarly to Find Next;, the program has to remember the location where the previous search stopped. So that you can start searching from that location. And this should happen even if the buffer were modified between the two searches.

We could store the match_end iter passed on gtk_text_iter_forward_search and use it as the starting point for the next search. But the problem is that if the buffer were modified in between, the iter would get invalidated. This takes us to marks.

4.2.1. Marks

A mark preserves a position in the buffer between modifications. This is possible because their behavior is defined when text is inserted or deleted. Quoting from gtk+ manual

When text containing a mark is deleted, the mark remains in the position originally occupied by the deleted text. When text is inserted at a mark, a mark with left gravity will be moved to the beginning of the newly-inserted text, and a mark with right gravity will be moved to the end.

The gravity of the mark is specified while creation. The following function can be used to create a mark associated with a buffer.


GtkTextMark* gtk_text_buffer_create_mark( GtkTextBuffer *buffer,
                                          const gchar *mark_name,
                                          const GtkTextIter *where,
                                          gboolean left_gravity );
The iter where specifies a position in the buffer which has to be marked. left_gravity determines how the mark moves when text is inserted at the mark. The mark_name is a string that can be used to identify the mark. If mark_name is specified, the mark can be retrieved using the following function.

GtkTextMark* gtk_text_buffer_get_mark( GtkTextBuffer *buffer,
                                       const gchar *name );
With named tags, you do not have to carry around a pointer to the marker, which can be easily retrieved using gtk_text_buffer_get_mark.

A mark by itself cannot be used for buffer operations, it has to converted into an iter just before buffer operations are to be performed. The following function can be used to convert a mark into iter


void gtk_text_buffer_get_iter_at_mark( GtkTextBuffer *buffer,
                                       GtkTextIter *iter,
                                       GtkTextMark *mark );

We now know sufficient functions to implement the Find Next functionality. Here's the code that does that.


#include <gtk/gtk.h>

void find (GtkTextView *text_view, const gchar *text, GtkTextIter *iter)
{
  GtkTextIter mstart, mend;
  GtkTextBuffer *buffer;
  gboolean found;

  buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (text_view));
  found = gtk_text_iter_forward_search (iter, text, 0, &mstart, &mend, NULL);

  if (found)
    {
      gtk_text_buffer_select_range (buffer, &mstart, &mend);
      gtk_text_buffer_create_mark (buffer, "last_pos", &mend, FALSE);
    }
}

#include <gtk/gtk.h>

typedef struct _App {
  GtkWidget *text_view;
  GtkWidget *search_entry;
} App;

void
win_destroy (void)
{
  gtk_main_quit();
}

void
next_button_clicked (GtkWidget *next_button, App *app)
{
  const gchar *text;
  GtkTextBuffer *buffer;
  GtkTextMark *last_pos;
  GtkTextIter iter;

  text = gtk_entry_get_text (GTK_ENTRY (app->search_entry));
  
  buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (app->text_view));

  last_pos = gtk_text_buffer_get_mark (buffer, "last_pos");
  if (last_pos == NULL)
    return;

  gtk_text_buffer_get_iter_at_mark (buffer, &iter, last_pos);
  find (GTK_TEXT_VIEW (app->text_view), text, &iter);
}

void
search_button_clicked (GtkWidget *search_button, App *app)
{
  const gchar *text;
  GtkTextBuffer *buffer;
  GtkTextIter iter;

  text = gtk_entry_get_text (GTK_ENTRY (app->search_entry));

  buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (app->text_view));
  gtk_text_buffer_get_start_iter (buffer, &iter);
  
  find (GTK_TEXT_VIEW (app->text_view), text, &iter);
}

int 
main (int argc, char *argv[])
{
  GtkWidget *win;
  GtkWidget *vbox;
  GtkWidget *hbox;
  GtkWidget *search_button;
  GtkWidget *next_button;
  GtkWidget *swindow;

  App app;

  gtk_init (&argc, &argv);

  win = gtk_window_new (GTK_WINDOW_TOPLEVEL);
  g_signal_connect (G_OBJECT (win), "destroy", win_destroy, NULL);

  vbox = gtk_vbox_new (FALSE, 2);
  gtk_container_add (GTK_CONTAINER (win), vbox);

  hbox = gtk_hbox_new (FALSE, 2);
  gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, FALSE, 0);
  
  app.search_entry = gtk_entry_new ();
  gtk_box_pack_start (GTK_BOX (hbox), app.search_entry, TRUE, TRUE, 0);

  search_button = gtk_button_new_with_label ("Search");  
  gtk_box_pack_start (GTK_BOX (hbox), search_button, FALSE, FALSE, 0);
  g_signal_connect (G_OBJECT (search_button), "clicked", 
                    G_CALLBACK (search_button_clicked), &app);

  next_button = gtk_button_new_with_label ("Next");
  gtk_box_pack_start (GTK_BOX (hbox), next_button, FALSE, FALSE, 0);
  g_signal_connect (G_OBJECT (next_button), "clicked",
                    G_CALLBACK (next_button_clicked), &app);

  swindow = gtk_scrolled_window_new (NULL, NULL);
  gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (swindow),
                                  GTK_POLICY_AUTOMATIC,
                                  GTK_POLICY_AUTOMATIC);
  gtk_box_pack_start (GTK_BOX (vbox), swindow, TRUE, TRUE, 0);

  app.text_view = gtk_text_view_new ();
  gtk_container_add (GTK_CONTAINER (swindow), app.text_view);

  gtk_widget_show_all (win);

  gtk_main();
  
  return 0;
}

4.3. The Scrolling Problem

There is a small problem with the above example. It does not scroll to the matched text. This can be irritating when the matched text is not in the visible portion of the buffer.

The function to scroll to a position in the buffer is


void gtk_text_view_scroll_mark_onscreen( GtkTextView *text_view,
                                         GtkTextMark *mark );
mark specifies the position to scroll to. Note that this is a method of the GtkTextView object rather than a buffer object. Since it does not change the contents of the buffer, it only changes the way a buffer is viewed

The example below uses gtk_text_view_scroll_mark_onscreen and fixes the scrolling problem.


void
find (GtkTextView *text_view, const gchar *text, GtkTextIter *iter)
{
  GtkTextIter mstart, mend;
  gboolean found;
  GtkTextBuffer *buffer;
  GtkTextMark *last_pos;

  buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (text_view));
  found = gtk_text_iter_forward_search (iter, text, 0, &mstart, &mend, NULL);

  if (found)
    {
      gtk_text_buffer_select_range (buffer, &mstart, &mend);
      last_pos = gtk_text_buffer_create_mark (buffer, "last_pos", 
                                              &mend, FALSE);
      gtk_text_view_scroll_mark_onscreen (text_view, last_pos);
    }
}

The gtk_text_view_scroll_mark_onscreen function scrolls just enough, for mark to be visible. But, what if you want mark to be centered, or to be the first line on the screen. This can be done using


void gtk_text_view_scroll_to_mark( GtkTextView *text_view,
                                   GtkTextMark *mark,
                                   gdouble within_margin,
                                   gboolean use_align,
                                   gdouble xalign,
                                   gdouble yalign );
The GTK manual explains this function -

Scrolls text_view so that mark is on the screen in the position indicated by xalign and yalign. An alignment of 0.0 indicates left or top, 1.0 indicates right or bottom, 0.5 means center. If use_align is FALSE, the text scrolls the minimal distance to get the mark onscreen, possibly not scrolling at all. The effective screen for purposes of this function is reduced by a margin of size within_margin.

4.4. More on Marks

When a mark is no longer required, it can be deleted using


void gtk_text_buffer_delete_mark( GtkTextBuffer *buffer,
                                  GtkTextMark *mark );

void gtk_text_buffer_delete_mark_by_name( GtkTextBuffer *buffer,
                                          const gchar *name );

4.5. Built-in Marks

There are two marks built-in to GtkTextBuffer -- "insert" and "selection_bound". The "insert" mark refers to the cursor position, also called the insertion point. A selection is bounded by two marks. One is the "insert" mark and the other is "selection_bound" mark. When no text is selected the two marks are in the same position.