Multiline Text Editing Widget Vijay Kumar B. vijaykumar@bravegnu.org $Name: GTKTEXT_0_4 $ $Id: gtktext.docbook,v 1.9 2007/01/07 11:17:21 vijay Exp $ Copyright ? 2005 Vijay Kumar B. Revision History Revision 0.4 7 Jan 2007 Revised by: bertrand Added GtkSourceView example. Revision 0.3 15 June 2006 Revised by: vijay Fixed document style and typos. Revision 0.2 23 May 2006 Revised by: vijay Fixed spelling mistakes and typos. Revision 0.1 16 May 2006 Revised by: vijay Initial revision. ------------------------------------------------------------------------------- Table of Contents 1. Simple Multiline Text Widget 1.1. More on Iterators 1.2. UTF-8 and GTK+ 1.3. Other Editing Functions 1.4. Other Functions to Obtain Iters 2. Formatted Text in GtkTextView 2.1. More Functions for Applying and Removing tags 2.2. Formatting the Entire Widget 3. Cut, Copy and Paste 4. Searching 4.1. Simple Search 4.2. Continuing your Search 4.2.1. Marks 4.3. The Scrolling Problem 4.4. More on Marks 4.5. Built-in Marks 5. Examining and Modifying Text 5.1. More functions to Examine and Modify Text 6. Images/Widgets 6.1. Inserting Images 6.2. Retrieving Images 6.3. Inserting Widgets 6.4. Retrieving Widgets 7. Buffer and Window Coordinates 7.1. Tooltips under Text Cursor 7.2. More on Buffer and Window Coordinates 8. Final Notes 8.1. GtkTextView in gtk-demo 8.2. GtkSourceView 9. Credits 10. Tutorial Copyright and Permissions Notice 1. Simple Multiline Text Widget In this section, you will learn how to create a simple multiline text widget, using GtkTextView, for data entry. The widget itself is created using GtkWidget *gtk_text_view_new( void ); When the GtkTextView widget is created this way, a GtkTextBuffer object associated with this widget is also created. The GtkTextBuffer is responsible for storing the text and associated attributes, while the GtkTextView widget is responsible for displaying the text ie. it provides an I/O interface to buffer. All text modification operations are related to the buffer object. The buffer associated with a GtkTextView widget can be obtained using GtkTextBuffer *gtk_text_view_get_buffer( GtkTextView *text_view ); Some common operations performed on a simple multiline text widget are, setting the entire text and reading the entire text from the buffer. The entire text of the buffer can be set using void gtk_text_buffer_set_text( GtkTextBuffer *buffer, const gchar *text, gint len ); The len should be specified when the text contains '\0'. When the text does not contain '\0'and is terminated by a '\0', len could be -1. Getting the entire text of a buffer, could be a little more complicated than setting the entire text of the buffer. You will have to understand iterators. Iterators are objects that represent positions between two characters in a buffer. Iters in a buffer can be obtained using many different functions, but for our simple case the following functions can be used to get the iters at the start and end of the buffer. void gtk_text_buffer_get_start_iter( GtkTextBuffer *buffer, GtkTextIter *iter ); void gtk_text_buffer_get_end_iter( GtkTextBuffer *buffer, GtkTextIter *iter ); Unlike other objects which are created using constructor like functions returning pointers to the objects, the GtkTextIter objects are created by instantiating the structure itself i.e they are allocated on the stack. So new GtkTextIter objects are created as follows GtkTextIter start_iter; GtkTextIter end_iter; Pointers to the iterators are then passed to the above functions for initialization. The initialized iters, can be used in the following function to retrieve the entire contents of the buffer. gchar *gtk_text_buffer_get_text( GtkTextBuffer *buffer, const GtkTextIter *start, const GtkTextIter *end, gboolean include_hidden_chars ); The include_hidden_chars is used to specify whether are not to include text that has the invisible attribute set. Text attributes and how to set them will be discussed later in this tutorial. The returned string is dynamically allocated and should be freed using g_free. Below is a sample program, that implements the simple multiline text widget. The program assigns a default text to the buffer. The text in the buffer can be modified by the user and when the close button is pressed it prints the contents of the buffer and quits. [simple] #include void on_window_destroy (GtkWidget *widget, gpointer data) { gtk_main_quit (); } /* Callback for close button */ void on_button_clicked (GtkWidget *button, GtkTextBuffer *buffer) { GtkTextIter start; GtkTextIter end; gchar *text; /* Obtain iters for the start and end of points of the buffer */ gtk_text_buffer_get_start_iter (buffer, &start); gtk_text_buffer_get_end_iter (buffer, &end); /* Get the entire buffer text. */ text = gtk_text_buffer_get_text (buffer, &start, &end, FALSE); /* Print the text */ g_print ("%s", text); g_free (text); gtk_main_quit (); } int main(int argc, char *argv[]) { GtkWidget *window; GtkWidget *vbox; GtkWidget *text_view; GtkWidget *button; GtkTextBuffer *buffer; gtk_init (&argc, &argv); /* Create a Window. */ window = gtk_window_new (GTK_WINDOW_TOPLEVEL); gtk_window_set_title (GTK_WINDOW (window), "Simple Multiline Text Input"); /* Set a decent default size for the window. */ gtk_window_set_default_size (GTK_WINDOW (window), 200, 200); g_signal_connect (G_OBJECT (window), "destroy", G_CALLBACK (on_window_destroy), NULL); vbox = gtk_vbox_new (FALSE, 2); gtk_container_add (GTK_CONTAINER (window), vbox); /* Create a multiline text widget. */ text_view = gtk_text_view_new (); gtk_box_pack_start (GTK_BOX (vbox), text_view, 1, 1, 0); /* Obtaining the buffer associated with the widget. */ buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (text_view)); /* Set the default buffer text. */ gtk_text_buffer_set_text (buffer, "Hello Text View!", -1); /* Create a close button. */ button = gtk_button_new_with_label ("Close"); gtk_box_pack_start (GTK_BOX (vbox), button, 0, 0, 0); g_signal_connect (G_OBJECT (button), "clicked", G_CALLBACK (on_button_clicked), buffer); gtk_widget_show_all (window); gtk_main (); return 0; } ------------------------------------------------------------------------------- 1.1. More on Iterators A point to note about text iterators - iterators are not valid indefinitely. Whenever the buffer is modified in a way that affects the number of characters in the buffer, all outstanding iterators become invalid. You will have to re-obtain iterators to use them. To preserve positions across buffer modifications the GtkTextMark can be used. Text marks will be discussed later in this tutorial. ------------------------------------------------------------------------------- 1.2. UTF-8 and GTK+ GTK+ handles text in UTF-8 format. For the uninitiated, the UTF-8 is an ASCII compatible multi-byte unicode encoding. The thing to be noted is that one character can be encoded as multiple bytes. The GTK+ manual uses the term offset for character counts, and uses the term index for byte counts. The len argument of the gtk_text_buffer_set_text function is the length of the text in bytes. ------------------------------------------------------------------------------- 1.3. Other Editing Functions The following function can be used to delete text from a buffer. void gtk_text_buffer_delete( GtkTextBuffer *buffer, GtkTextIter *start, GtkTextIter *end ); Since this function modifies the buffer, all outstanding iterators become invalid after a call to this function. But, the iterators passed to the function are re-initialized to point to the location where the text was deleted. The following function can be used to insert text into a buffer at a position specified by an iterator. void gtk_text_buffer_insert( GtkTextBuffer *buffer, GtkTextIter *iter, const gchar *text, gint len ); The len argument is similar to the len argument in gtk_text_buffer_set_text function. As with gtk_text_buffer_delete, the buffer is modified and hence all outstanding iterators become invalid, and start and end are re-initialized. Hence the same iterator can be used for a series of consecutive inserts. The following function can be used to insert text at the current cursor position. void gtk_text_buffer_insert_at_cursor( GtkTextBuffer *buffer, const gchar *text, gint len ); ------------------------------------------------------------------------------- 1.4. Other Functions to Obtain Iters The following function can be used to get iterators at the beginning and end of the buffer in one go. void gtk_text_buffer_get_bounds( GtkTextBuffer *buffer, GtkTextIter *start, GtkTextIter *end ); A variant of the above function can be used to obtain iterators at the beginning and end of the current selection. void gtk_text_buffer_selection_bounds( GtkTextBuffer *buffer, GtkTextIter *start, GtkTextIter *end ); The following functions can be used to obtain an iterator at a specified character offset into the buffer, at the start of the given line, at an character offset into a given line, or at a byte offset into a given line, respectively. void gtk_text_buffer_get_iter_at_offset( GtkTextBuffer *buffer, GtkTextIter *iter, gint char_offset); void gtk_text_buffer_get_iter_at_line( GtkTextBuffer *buffer, GtkTextIter *iter, gint line_number); void gtk_text_buffer_get_iter_at_line_offset( GtkTextBuffer *buffer, GtkTextIter *iter, gint line_no, gint offset ); void gtk_text_buffer_get_iter_at_line_index( GtkTextBuffer *buffer, GtkTextIter *iter, gint line_no, gint index ); ------------------------------------------------------------------------------- 2. Formatted Text in GtkTextView GtkTextView can also be used to display formatted text. This usually involves creating tags which represent a group of attributes and then applying them to a range of text. Tag objects are associated with a buffer and are created using the following function, GtkTextTag* gtk_text_buffer_create_tag( GtkTextBuffer *buffer, const gchar *tag_name, const gchar *first_prop_name, ... ); Tags can be optionally associated with a name tag_name. Thus, the tag could be referred using the returned pointer or using the tag_name. For anonymous tags, NULL is passed to tag_name. The group of properties represented by this tag is listed as name/value pairs after the tag_name. The list of property/value pairs is terminated with a NULL pointer. "style", "weight", "editable", "justification" are some common property names. The following table lists their meaning and assignable values. Table 1. Common properties used for creating tags +-----------------------------------------------------------------------------+ | Property | Meaning | Values | |---------------+--------------------+----------------------------------------| | |Font style as |PANGO_STYLE_NORMAL | |"style" |PangoStyle. |PANGO_STYLE_OBLIQUE | | | |PANGO_STYLE_ITALIC | |---------------+--------------------+----------------------------------------| |"weight" |Font weight as |PANGO_WEIGHT_NORMAL | | |integer. |PANGO_WEIGHT_BOLD | |---------------+--------------------+----------------------------------------| |"editable" |Text modifiable by |TRUE | | |user. |FALSE | |---------------+--------------------+----------------------------------------| | | |GTK_JUSTIFY_LEFT | |"justification"|Justification of |GTK_JUSTIFY_RIGHT | | |text. |GTK_JUSTIFY_CENTER | | | |GTK_JUSTIFY_FILL | |---------------+--------------------+----------------------------------------| |"foreground" |Foreground color of |"#RRGGBB" | | |text. | | |---------------+--------------------+----------------------------------------| |"background" |Background color of |"#RRGGBB" | | |text. | | |---------------+--------------------+----------------------------------------| | | |GTK_WRAP_NONE - Don't wrap text | | | |GTK_WRAP_CHAR - Wrap text, breaking in | | | |between characters | |"wrap-mode" |Text wrapping mode |GTK_WRAP_WORD - Wrap text, breaking in | | | |between words | | | |GTK_WRAP_WORD_CHAR - Wrap text, breaking| | | |in words, or if that is not enough, also| | | |between characters | |---------------+--------------------+----------------------------------------| | |Text font specified | | |"font" |by font description |"[FAMILY-LIST] [STYLE-OPTIONS] [SIZE]" | | |string. | | +-----------------------------------------------------------------------------+ Here is a brief description of the font description string from the GTK+ manual. ""[FAMILY-LIST] [STYLE-OPTIONS] [SIZE]", where FAMILY-LIST is a comma separated list of families optionally terminated by a comma, STYLE_OPTIONS is a whitespace separated list of words where each WORD describes one of style, variant, weight, or stretch, and SIZE is an decimal number (size in points). Any one of the options may be absent. If FAMILY-LIST is absent, then the family_name field of the resulting font description will be initialized to NULL. If STYLE-OPTIONS is missing, then all style options will be set to the default values. If SIZE is missing, the size in the resulting font description will be set to 0." See the GTK+ manual, for a complete list of properties and their corresponding values. The created tag can then be applied to a range of text using the following functions, void gtk_text_buffer_apply_tag( GtkTextBuffer *buffer, GtkTextTag *tag, const GtkTextIter *start, const GtkTextIter *end ); void gtk_text_buffer_apply_tag_by_name( GtkTextBuffer *buffer, const gchar *name const GtkTextIter *start, const GtkTextIter *end ); The first function specifies the tag to be applied by a tag object and the second function specifies the tag by it's name. The range of text over with the tag is to applies is specified by the start and end iters. Below is an extension of the previous example, that has a tool-bar to apply different tags to selected regions of text. [format] #include void on_window_destroy (GtkWidget *widget, gpointer data) { gtk_main_quit (); } /* Callback for buttons in the toolbar. */ void on_format_button_clicked (GtkWidget *button, GtkTextBuffer *buffer) { GtkTextIter start, end; gchar *tag_name; /* Get iters at the beginning and end of current selection. */ gtk_text_buffer_get_selection_bounds (buffer, &start, &end); /* Find out what tag to apply. The key "tag" is set to the appropriate tag name when the button widget was created. */ tag_name = g_object_get_data (G_OBJECT (button), "tag"); /* Apply the tag to the selected text. */ gtk_text_buffer_apply_tag_by_name (buffer, tag_name, &start, &end); } /* Callback for the close button. */ void on_close_button_clicked (GtkWidget *button, GtkTextBuffer *buffer) { GtkTextIter start; GtkTextIter end; gchar *text; /* Get iters at the beginning and end of the buffer. */ gtk_text_buffer_get_bounds (buffer, &start, &end); /* Retrieve the text. */ text = gtk_text_buffer_get_text (buffer, &start, &end, FALSE); /* Print the text. */ g_print ("%s", text); g_free (text); gtk_main_quit (); } int main(int argc, char *argv[]) { GtkWidget *window; GtkWidget *vbox; GtkWidget *bbox; GtkWidget *bold_button; GtkWidget *italic_button; GtkWidget *font_button; GtkWidget *text_view; GtkTextBuffer *buffer; GtkWidget *close_button; gtk_init (&argc, &argv); window = gtk_window_new (GTK_WINDOW_TOPLEVEL); gtk_window_set_title (GTK_WINDOW (window), "Formatted multiline text widget"); gtk_window_set_default_size (GTK_WINDOW (window), 200, 200); g_signal_connect (G_OBJECT (window), "destroy", G_CALLBACK (on_window_destroy), NULL); vbox = gtk_vbox_new (FALSE, 2); gtk_container_add (GTK_CONTAINER (window), vbox); /* Create a button box that will serve as a toolbar. */ bbox = gtk_hbutton_box_new (); gtk_box_pack_start (GTK_BOX (vbox), bbox, 0, 0, 0); /* Create the text view widget and set some default text. */ text_view = gtk_text_view_new (); gtk_box_pack_start (GTK_BOX (vbox), text_view, 1, 1, 0); buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (text_view)); gtk_text_buffer_set_text (buffer, "Hello Text View!", -1); /* Create tags associated with the buffer. */ /* Tag with weight bold and tag name "bold" . */ gtk_text_buffer_create_tag (buffer, "bold", "weight", PANGO_WEIGHT_BOLD, NULL); /* Tag with style italic and tag name "italic". */ gtk_text_buffer_create_tag (buffer, "italic", "style", PANGO_STYLE_ITALIC, NULL); /* Tag with font fixed and tag name "font". */ gtk_text_buffer_create_tag (buffer, "font", "font", "fixed", NULL); /* Create button for bold and add them the to the tool bar. */ bold_button = gtk_button_new_with_label ("Bold"); gtk_container_add (GTK_CONTAINER (bbox), bold_button); /* Connect the common signal handler on_format_button_clicked. This signal handler is common to all the buttons. A key called "tag" is associated with the buttons, which speicifies the tag name that the button is supposed to apply. The handler reads this key and applies the appropriate tag. Thus only one handler is needed for any number of buttons in the toolbar. */ g_signal_connect (G_OBJECT (bold_button), "clicked", G_CALLBACK (on_format_button_clicked), buffer); g_object_set_data (G_OBJECT (bold_button), "tag", "bold"); /* Create button for italic. */ italic_button = gtk_button_new_with_label ("Italic"); gtk_container_add (GTK_CONTAINER (bbox), italic_button); g_signal_connect (G_OBJECT (italic_button), "clicked", G_CALLBACK (on_format_button_clicked), buffer); g_object_set_data (G_OBJECT (italic_button), "tag", "italic"); /* Create button for fixed font. */ font_button = gtk_button_new_with_label ("Font Fixed"); gtk_container_add (GTK_CONTAINER (bbox), font_button); g_signal_connect (G_OBJECT (font_button), "clicked", G_CALLBACK (on_format_button_clicked), buffer); g_object_set_data (G_OBJECT (font_button), "tag", "font"); /* Create the close button. */ close_button = gtk_button_new_with_label ("Close"); gtk_box_pack_start (GTK_BOX (vbox), close_button, 0, 0, 0); g_signal_connect (G_OBJECT (close_button), "clicked", G_CALLBACK (on_close_button_clicked), buffer); gtk_widget_show_all (window); gtk_main (); return 0; } ------------------------------------------------------------------------------- 2.1. More Functions for Applying and Removing tags In the previous section, the gtk_text_buffer_insert function was introduced. A variant of this function can be used to insert text with tags applied. void gtk_text_buffer_insert_with_tags( GtkTextBuffer *buffer, GtkTextIter *iter, const gchar *text, gint len, GtkTextTag *first_tag, ... ); The tags argument list is terminated by a NULL pointer. The _by_name suffixed variants are also available, in which the tags to be applied are specified by the tag names. Tags applied to a range of text can be removed by using the following function void gtk_text_buffer_remove_tag( GtkTextBuffer *buffer, GtkTextTag *tag, const GtkTextIter *start, const GtkTextIter *end ); This function also has the _by_name prefixed variant. All tags on a range of text can be removed in one go using the following function void gtk_text_buffer_remove_all_tags( GtkTextBuffer *buffer, const GtkTextIter *start, const GtkTextIter *end ); ------------------------------------------------------------------------------- 2.2. Formatting the Entire Widget The above functions apply attributes to portions of text in a buffer. If attributes have to be applied for the entire GtkTextView widget, the gtk_text_view_set_* functions can be used. For example, the following function makes text_view editable/non-editable. void gtk_text_view_set_editable( GtkTextView *text_view, gboolean setting ); See the GTK+ manual for a complete list of available functions. The attributes set by these functions, on the entire widget, can be overridden by applying tags to portions of text in the buffer. ------------------------------------------------------------------------------- 3. Cut, Copy and Paste In this section you will learn how to do common clipboard related activities like cut, copy and paste. First, you will have to get hold of a clipboard object using GtkClipboard *gtk_clipboard_get( GdkAtom selection ); Usually, a value of GDK_NONE is passed to selection, which gives the default clipboard. A value of GDK_SELECTION_PRIMARY identifies the primary X selection. Selected text can be then copied to the clipboard using void gtk_text_buffer_copy_clipboard( GtkTextBuffer *buffer, GtkClipboard *clipboard ); The clipboard is a clipboard object obtained from gtk_clipboard_get. Selected text can be cut to the clipboard using void gtk_text_buffer_cut_clipboard( GtkTextBuffer *buffer, GtkClipboard *clipboard, gboolean default_editable ); For portions of the selected text that do not have the editable tag applied, the edit-ability is assumed from default_editable. Text can be pasted from the clipboard using void gtk_text_buffer_paste_clipboard( GtkTextBuffer *buffer, GtkClipboard *clipboard, GtkTextIter *override_location, gboolean default_editable ); If override_location is not NULL, text is inserted at the iter specified by override_location. Else, text is inserted at the current cursor location. ------------------------------------------------------------------------------- 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. [search] #include 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. [search_nex] #include 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 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. ------------------------------------------------------------------------------- 5. Examining and Modifying Text Examining and modifying text is yet another common operation performed on text buffers. Examples: converting a selected portion of text into a comment while editing a program, determining and inserting the correct end tag while editing HTML, inserting a pair of HTML tags around the current word, etc. The GtkTextIter object provides functions to do such processing. In this section we will develop two programs to demonstrate these functions. The first program will insert start/end li tags(not to be confused with text attribute tags) around the current line, when a button is clicked. The second program will insert an end tag for an unclosed start tag. To insert tags around the current line, we first obtain an iter at the current cursor position. Then we move the iter to the beginning of the line, insert the start tag, move the iter to the end of the line, and insert the end tag. An iter can be moved to a specified offset in the same line using void gtk_text_iter_set_line_offset( GtkTextIter *iter, gint char_on_line ); The function moves iter within the line, to the character offset specified by char_on_line. If char_on_line is equal to the no. of characters in the line, the iter is moved to the start of the next line. A character offset of zero, will move the iter to the beginning of the line. The iter can be moved to the end of the line using gboolean gtk_text_iter_forward_to_line_end( GtkTextIter *iter ); Now that we know the functions required to implement the first program, here's the code. #include void on_window_destroy (GtkWidget *widget, gpointer data) { gtk_main_quit (); } /* Callback for close button */ void on_button_clicked (GtkWidget *button, GtkTextBuffer *buffer) { GtkTextIter iter; GtkTextIter end; GtkTextMark *cursor; gchar *text; /* Get the mark at cursor. */ cursor = gtk_text_buffer_get_mark (buffer, "insert"); /* Get the iter at cursor. */ gtk_text_buffer_get_iter_at_mark (buffer, &iter, cursor); gtk_text_iter_set_line_offset (&iter, 0); gtk_text_buffer_insert (buffer, &iter, "
  • ", -1); gtk_text_iter_forward_to_line_end (&iter); gtk_text_buffer_insert (buffer, &iter, "
  • ", -1); } int main(int argc, char *argv[]) { GtkWidget *window; GtkWidget *vbox; GtkWidget *text_view; GtkWidget *button; GtkTextBuffer *buffer; gtk_init (&argc, &argv); /* Create a Window. */ window = gtk_window_new (GTK_WINDOW_TOPLEVEL); gtk_window_set_title (GTK_WINDOW (window), "Insert Tags"); /* Set a decent default size for the window. */ gtk_window_set_default_size (GTK_WINDOW (window), 200, 200); g_signal_connect (G_OBJECT (window), "destroy", G_CALLBACK (on_window_destroy), NULL); vbox = gtk_vbox_new (FALSE, 2); gtk_container_add (GTK_CONTAINER (window), vbox); /* Create a multiline text widget. */ text_view = gtk_text_view_new (); gtk_box_pack_start (GTK_BOX (vbox), text_view, 1, 1, 0); /* Obtaining the buffer associated with the widget. */ buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (text_view)); /* Set the default buffer text. */ gtk_text_buffer_set_text (buffer, "Item1\nItem2\nItem3", -1); /* Create a insert bold tags button. */ button = gtk_button_new_with_label ("Make List Item"); gtk_box_pack_start (GTK_BOX (vbox), button, 0, 0, 0); g_signal_connect (G_OBJECT (button), "clicked", G_CALLBACK (on_button_clicked), buffer); gtk_widget_show_all (window); gtk_main (); return 0; } For the second program, we will have to first get the iter at the current cursor position. We then search backwards from the cursor position, through the buffer till we hit on an unclosed tag. We then insert the corresponding end tag at the current cursor position. (Note that the procedure given does not take care of many special cases, and might not be the best way to determine an unclosed tag. But it serves our purpose of explaining text manipulation functions. Developing a perfect algorithm to determine an unclosed tag, is out of the scope of this tutorial.) We can identify tags using the left angle bracket. So searching for start/end tags involves search for the left angle bracket. This can be done using gboolean gtk_text_iter_backward_find_char( GtkTextIter *iter, GtkTextCharPredicate pred, gpointer user_data, const GtkTextIter *limit ); The function proceeds backwards from iter, and calls pred for each character in the buffer, with the character and user_data as arguments, till pred returns TRUE. (pred should return TRUE when a match is found.) If a match is found, the function moves iter to the matching position and returns TRUE. If a match is not found, the function moves iter to the beginning of the buffer or limit(if non-NULL) and returns FALSE. For our purpose we write a predicate that returns TRUE when the character is a left angle bracket. When we hit on a left angle bracket we check whether the corresponding tag is a start tag or an end tag. This is done by examining the character immediately after the left angle bracket. If it is a '/' it is an end tag. To extract the character after the angle bracket we move the left angle bracket iter by one character. And then extract the character at that position. To move an iter forward by one character, the following function can be used. gboolean gtk_text_iter_forward_char( GtkTextIter *iter ); To extract the character at an iter the following function can be used. gunichar gtk_text_iter_get_char( const GtkTextIter *iter ); After determining the tag type we do the following, * If the tag is an end tag, we push the tag name into a stack and then proceed to find more tags. * If it is a start tag, we pop out it's matching tag from the stack. While poping out, if there were no more items in the stack, we have hit on an unmatched start tag! We then insert the corresponding end tag at the current cursor position. We haven't mentioned how we extract the tag name. The tag name is extracted using two iters(start and end iter). The start iter is obtained by starting from the left angle bracket iter and searching for an alphanumeric character, in the forward direction. The end iter is obtained by starting from the start iter and searching for a non-alphanumeric character, in the forward direction. The search can be done using the forward variant of the gtk_text_iter_backward_find_char. The code for the second example follows. #include #include #include void on_window_destroy (GtkWidget *widget, gpointer data) { gtk_main_quit (); } gboolean islangle (gunichar ch, gpointer data) { if (ch == '<') return TRUE; else return FALSE; } gboolean notalnum (gunichar ch1, gpointer data) { return !isalnum (ch1); } /* * Check whether the tag at ITER is an opening tag or a closing tag. */ gboolean is_closing (GtkTextIter *iter, GtkTextBuffer *buffer) { GtkTextIter slash; slash = *iter; gtk_text_iter_forward_char (&slash); if (gtk_text_iter_get_char (&slash) == '/') return TRUE; else return FALSE; } /* * Returns the start/end tag at position specified by ITER. * Returns NULL if tag not found. */ char * get_this_tag (GtkTextIter *iter, GtkTextBuffer *buffer) { GtkTextIter start_tag = *iter; GtkTextIter end_tag; gboolean found; /* start_tag points to '<', moving to the next alphabet character will get the start of the tag name. */ found = gtk_text_iter_forward_find_char (&start_tag, (GtkTextCharPredicate) isalnum, NULL, NULL); if (!found) return NULL; /* search for non-alnum character in the forward direction from start_tag */ end_tag = start_tag; found = gtk_text_iter_forward_find_char (&end_tag, (GtkTextCharPredicate) notalnum, NULL, NULL); if (!found) return NULL; /* return the text between '<[/]' and non-alnum */ return gtk_text_buffer_get_text (buffer, &start_tag, &end_tag, FALSE); } /* * Insert the closing tag specified by TAG at the current cursor * position. */ void insert_closing_tag (GtkTextIter *iter, gchar *tag, GtkTextBuffer *buffer) { char *insert; insert = g_strdup_printf ("", tag); gtk_text_buffer_insert_at_cursor(buffer, insert, strlen(insert)); g_free (insert); } /* Callback for insert closing tag button */ void on_button_clicked (GtkWidget *button, GtkTextBuffer *buffer) { GtkTextIter iter; GQueue *stack; GtkTextMark *cursor; stack = g_queue_new (); /* Get the mark at cursor. */ cursor = gtk_text_buffer_get_mark (buffer, "insert"); /* Get the iter at cursor. */ gtk_text_buffer_get_iter_at_mark (buffer, &iter, cursor); while (1) { int found; char *tag; char *tag_in_stack; /* Search backwards for '<'. */ found = gtk_text_iter_backward_find_char (&iter, islangle, NULL, NULL); if (!found) break; tag = get_this_tag (&iter, buffer); if (tag == NULL) continue; if (is_closing (&iter, buffer)) { /* If it is a closing tag, push it into the stack */ g_queue_push_head(stack, tag); } else { /* If it is an opening tag, pop an item from the stack. If there are no items in the stack, then this tag has not been closed, and it is the one we should close. */ tag_in_stack = g_queue_pop_head(stack); if (tag_in_stack == NULL) { insert_closing_tag (&iter, tag, buffer); g_free (tag); break; } else g_free (tag_in_stack); } } g_queue_foreach (stack, (GFunc)g_free, NULL); g_queue_free (stack); } int main(int argc, char *argv[]) { GtkWidget *window; GtkWidget *vbox; GtkWidget *text_view; GtkWidget *button; GtkTextBuffer *buffer; gtk_init (&argc, &argv); /* Create a Window. */ window = gtk_window_new (GTK_WINDOW_TOPLEVEL); gtk_window_set_title (GTK_WINDOW (window), "Close Tag"); /* Set a decent default size for the window. */ gtk_window_set_default_size (GTK_WINDOW (window), 200, 200); g_signal_connect (G_OBJECT (window), "destroy", G_CALLBACK (on_window_destroy), NULL); vbox = gtk_vbox_new (FALSE, 2); gtk_container_add (GTK_CONTAINER (window), vbox); /* Create a multiline text widget. */ text_view = gtk_text_view_new (); gtk_box_pack_start (GTK_BOX (vbox), text_view, 1, 1, 0); /* Obtaining the buffer associated with the widget. */ buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (text_view)); /* Set the default buffer text. */ gtk_text_buffer_set_text (buffer, "\n" "Title\n" "\n" "

    Heading

    \n", -1); /* Create a close button. */ button = gtk_button_new_with_label ("Insert Close Tag"); gtk_box_pack_start (GTK_BOX (vbox), button, 0, 0, 0); g_signal_connect (G_OBJECT (button), "clicked", G_CALLBACK (on_button_clicked), buffer); gtk_widget_show_all (window); gtk_main (); return 0; } ------------------------------------------------------------------------------- 5.1. More functions to Examine and Modify Text There a lot more functions to examine and modify text. Some of the interesting ones are listed below. You can get the complete list of available functions from the GTK+ manual. There is a class of functions used to test for some characteristic of the text. For example to check whether the iter is at the beginning/end of word, sentence or line. The corresponding functions are gboolean gtk_text_iter_starts_word( const GtkTextIter *iter ); gboolean gtk_text_iter_ends_word( const GtkTextIter *iter ); gboolean gtk_text_iter_starts_sentence( const GtkTextIter *iter ); gboolean gtk_text_iter_ends_sentence( const GtkTextIter *iter ); gboolean gtk_text_iter_starts_line( const GtkTextIter *iter ); gboolean gtk_text_iter_ends_line( const GtkTextIter *iter ); The family of functions based on tags and tag toggling have also not been mentioned so far. To check whether a position in the buffer starts/ends/toggles a tag the following functions can be used. gboolean gtk_text_iter_begins_tag( const GtkTextIter *iter, GtkTextTag *tag ); gboolean gtk_text_iter_ends_tag( const GtkTextIter *iter, GtkTextTag *tag ); gboolean gtk_text_iter_toggles_tag( const GtkTextIter *iter, GtkTextTag *tag ); The return value of toggles_tag variant is the logical OR of begins_tag variant and ends_tag variant. If tag is NULL, the function returns TRUE if iter starts/ ends/toggles any tag. We can also move through a buffer based on tag toggling. To move to a position in the buffer where a particular tag toggles the following functions can be used. gboolean gtk_text_iter_forward_to_tag_toggle( GtkTextIter *iter, GtkTextTag *tag ); gboolean gtk_text_iter_backward_to_tag_toggle( GtkTextIter *iter, GtkTextTag *tag ); If tag is NULL, toggling of any tag is considered. ------------------------------------------------------------------------------- 6. Images/Widgets A text buffer can hold images and anchor location for widgets. In this section, you will learn how to insert images and widgets. You will also learn how to retrieve an inserted image/widget. ------------------------------------------------------------------------------- 6.1. Inserting Images An image can be inserted into a buffer using the following function void gtk_text_buffer_insert_pixbuf( GtkTextBuffer *buffer, GtkTextIter *iter, GdkPixbuf *pixbuf ); An image represented by pixbuf is inserted at iter. The pixbuf can be created from an image file using gdk_pixbuf_new_from_file. See the gdk manual for more details about GdkPixbuf objects. The example program given below takes in an image filename and inserts the corresponding image into a buffer. [image] #include typedef struct _App { GtkWidget *window; GtkWidget *text_view; } App; void on_window_destroy (GtkWidget *widget, gpointer data) { gtk_main_quit (); } void insert_button_clicked (GtkWidget *widget, gpointer data) { App *app = (App *)data; GtkWidget *dialog; int ret; gchar *filename; GError *error = NULL; GdkPixbuf *pixbuf; GtkTextBuffer *buffer; GtkTextMark *cursor; GtkTextIter iter; dialog = gtk_file_chooser_dialog_new ("Image file...", GTK_WINDOW (app->window), GTK_FILE_CHOOSER_ACTION_OPEN, GTK_STOCK_OK, GTK_RESPONSE_ACCEPT, GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, NULL); ret = gtk_dialog_run (GTK_DIALOG (dialog)); switch (ret) { case GTK_RESPONSE_ACCEPT: filename = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (dialog)); gtk_widget_destroy (dialog); break; case GTK_RESPONSE_DELETE_EVENT: case GTK_RESPONSE_CANCEL: gtk_widget_destroy (dialog); /* Fall through */ case GTK_RESPONSE_NONE: return; } pixbuf = gdk_pixbuf_new_from_file (filename, &error); g_free (filename); if (error) { GtkWidget *msg; msg = gtk_message_dialog_new (GTK_WINDOW (app->window), GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, error->message); gtk_dialog_run (GTK_DIALOG (msg)); gtk_widget_destroy (msg); g_error_free (error); return; } buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (app->text_view)); cursor = gtk_text_buffer_get_insert (buffer); gtk_text_buffer_get_iter_at_mark (buffer, &iter, cursor); gtk_text_buffer_insert_pixbuf (buffer, &iter, pixbuf); } int main(int argc, char *argv[]) { App app; GtkWidget *vbox; GtkWidget *insert_button; gtk_init (&argc, &argv); /* Create a Window. */ app.window = gtk_window_new (GTK_WINDOW_TOPLEVEL); gtk_window_set_title (GTK_WINDOW (app.window), "Insert image!"); /* Set a decent default size for the window. */ gtk_window_set_default_size (GTK_WINDOW (app.window), 200, 200); g_signal_connect (G_OBJECT (app.window), "destroy", G_CALLBACK (on_window_destroy), NULL); vbox = gtk_vbox_new (FALSE, 2); gtk_container_add (GTK_CONTAINER (app.window), vbox); insert_button = gtk_button_new_with_label("Insert Image"); gtk_box_pack_start (GTK_BOX (vbox), insert_button, FALSE, FALSE, 0); g_signal_connect (G_OBJECT (insert_button), "clicked", G_CALLBACK (insert_button_clicked), &app); /* Create a multiline text widget. */ app.text_view = gtk_text_view_new (); gtk_box_pack_start (GTK_BOX (vbox), app.text_view, 1, 1, 0); gtk_widget_show_all (app.window); gtk_main (); return 0; } ------------------------------------------------------------------------------- 6.2. Retrieving Images Images in a buffer are represented by the character 0xFFFC(Unicode object replacement character). When text containing images is retrieved from a buffer using gtk_text_buffer_get_text the 0xFFFC characters representing images are dropped off in the returned text. If these characters representing images are required, use the slice variant - gtk_text_buffer_get_slice. The image at a given position can be retrieved using GdkPixbuf* gtk_text_iter_get_pixbuf( const GtkTextIter *iter ); ------------------------------------------------------------------------------- 6.3. Inserting Widgets Inserting a widget, unlike inserting an image, is a two step process. The additional complexity is due to the functionality split between GtkTextView and GtkTextBuffer. The first step is to create and insert a GtkTextChildAnchor. A widget is held in a buffer using a GtkTextChildAnchor. A child anchor according to the GTK manual is a spot in the buffer where child widgets can be anchored. A child anchor can be created and inserted into a buffer using GtkTextChildAnchor* gtk_text_buffer_create_child_anchor( GtkTextBuffer *buffer, GtkTextIter *iter ); Where iter specifies the position in the buffer, where the widget is to be inserted. The next step is to add a child widget to the text view, at the anchor location. void gtk_text_view_add_child_at_anchor( GtkTextView *text_view, GtkWidget *child, GtkTextChildAnchor *anchor ); An anchor can hold only one widget(it could be a container widget, which in turn can contain many widgets), unless you are doing tricky things like displaying the same buffer using different GtkTextView objects. The following program inserts a button widget into a text buffer, whenever the user clicks on the Insert button. #include typedef struct _App { GtkWidget *window; GtkWidget *text_view; } App; void on_window_destroy (GtkWidget *widget, gpointer data) { gtk_main_quit (); } void insert_button_clicked (GtkWidget *widget, gpointer data) { App *app = (App *)data; GtkTextBuffer *buffer; GtkTextMark *cursor; GtkTextIter iter; GtkTextChildAnchor *anchor; GtkWidget *button; buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (app->text_view)); cursor = gtk_text_buffer_get_insert (buffer); gtk_text_buffer_get_iter_at_mark (buffer, &iter, cursor); anchor = gtk_text_buffer_create_child_anchor (buffer, &iter); button = gtk_button_new_with_label ("New button!"); gtk_text_view_add_child_at_anchor (GTK_TEXT_VIEW (app->text_view), button, anchor); gtk_widget_show (button); } int main(int argc, char *argv[]) { App app; GtkWidget *vbox; GtkWidget *insert_button; gtk_init (&argc, &argv); /* Create a Window. */ app.window = gtk_window_new (GTK_WINDOW_TOPLEVEL); gtk_window_set_title (GTK_WINDOW (app.window), "Insert button!"); /* Set a decent default size for the window. */ gtk_window_set_default_size (GTK_WINDOW (app.window), 200, 200); g_signal_connect (G_OBJECT (app.window), "destroy", G_CALLBACK (on_window_destroy), NULL); vbox = gtk_vbox_new (FALSE, 2); gtk_container_add (GTK_CONTAINER (app.window), vbox); insert_button = gtk_button_new_with_label ("Insert button"); gtk_box_pack_start (GTK_BOX (vbox), insert_button, FALSE, FALSE, 0); g_signal_connect (G_OBJECT (insert_button), "clicked", G_CALLBACK (insert_button_clicked), &app); /* Create a multiline text widget. */ app.text_view = gtk_text_view_new (); gtk_box_pack_start (GTK_BOX (vbox), app.text_view, 1, 1, 0); gtk_widget_show_all (app.window); gtk_main (); return 0; } ------------------------------------------------------------------------------- 6.4. Retrieving Widgets Child anchors are also represented in the buffer using the object replacement character 0xFFFC. Retrieving a widget is also a two step process. First, the child anchor has to be retrieved. This can be done using GtkTextChildAnchor* gtk_text_iter_get_child_anchor( const GtkTextIter *iter ); Next, the widget(s) associated with the child anchor has to be retrieved. This can be done using GList* gtk_text_child_anchor_get_widgets( GtkTextChildAnchor *anchor ); The function returns a list of widgets. As mentioned earlier, if you are not doing tricky things like multiple views for the same buffer, you will find only one widget in this list. The list has to be freed with g_list_free. ------------------------------------------------------------------------------- 7. Buffer and Window Coordinates Sometimes it is necessary to know the position of the text cursor on the screen, or the word in a buffer under the mouse cursor. For example, when you want to display the prototype of a function as a tooltip, when the user types open parenthesis. To do this, you will have to understand buffer coordinates and window coordinates. Both the buffer and window coordinates are pixel level coordinates. The difference is that the window coordinates takes into account only the portion of the buffer displayed on the screen. The concept would be better explained using a diagram. The large white box (with grid lines) in the following diagram depicts the text buffer. And the smaller inner grey box is the visible portion of the text buffer, displayed by the text view widget. [buffercoor] The buffer coordinates of the red dot(represented as (x, y)) is (4, 3). But the window coordinates of the red dot is (2, 1). This is because the window coordinates are calculated relative the visible portion of the text buffer. Similarly, the buffer coordinates of the blue dot is (3, 5) and the window coordinates is (1, 3). ------------------------------------------------------------------------------- 7.1. Tooltips under Text Cursor In this section, you will learn how to display tooltips under the text cursor. The procedure is as follows, * The buffer coordinates of the text cursor is obtained. * The buffer coordinates is converted to window coordinates (x1, y1). * The position (x2, y2) of the text view widget on the screen is obtained. * A window with the tooltip is displayed at (x1+x2, y1+y2). The buffer coordinates of a particular character in a buffer can be obtained using void gtk_text_view_get_iter_location( GtkTextView *text_view, const GtkTextIter *iter, GdkRectangle *location ); The function gets the rectangle that contains the character at iter and store it in location. The x and y members of location gives us the buffer coordinates. The buffer coordinates can be converted into window coordinates using void gtk_text_view_buffer_to_window_coords( GtkTextView *text_view, GtkTextWindowType win, gint buffer_x, gint buffer_y, gint *window_x, gint *window_y ); The function converts buffer coordinates (buffer_x, buffer_y), to window coordinates (window_x, window_y). You will have to pass GTK_TEXT_WINDOW_WIDGET for win, for things to work properly.(FIXME: What's the meaning of the other values of GtkTextWidowType?) Now that we know the position of the character within the text view widget, we will have to find the position of the text view widget on the screen. Each GTK widget has a corresponding GdkWindow associated with it. Once we know the GdkWindow associated with a widget, we can obtain it's X-Y coordinates using gdk_window_get_origin. The GdkWindow of the text view widget can be obtained using, GdkWindow* gtk_text_view_get_window( GtkTextView *text_view, GtkTextWindowType win ); Here again you will have to pass GTK_TEXT_WINDOW_WIDGET for win. We now know the functions required to display a tooltip under the text cursor. Before we proceed to the example, you will have to know which signal has to be trapped to do display the tooltip. Since we want the tooltip to be displayed when the user inserts open parenthesis, "insert_text" emitted by the buffer object can be used. As the signal's name suggests it is called whenever the user inserts text into the buffer. The callback prototype is void (*insert_text)( GtkTextBuffer *textbuffer, GtkTextIter *pos, gchar *text, gint length, gpointer user_data ) The function is called with position after the inserted text pos, the inserted text text and the length of the inserted text length. Below is an example program that displays a tooltip for the printf family of functions. [tip] #include /* List of functions and their corresponding tool tips. */ static char *tips[][2] = { {"printf", "(const char *format, ...)"}, {"fprintf", "(FILE *stream, const char *format, ...)"}, {"sprintf", "(char *str, const char *format, ...)"}, {"snprintf", "(char *str, size_t size, const char *format, ...)"}, {"fputc", "(int c, FILE *stream)"}, {"fputs", "(const char *s, FILE *stream)"}, {"putc", "(int c, FILE *stream)"}, {"putchar", "(int c)"}, {"puts", "(const char *s)"}, }; #define NUM_TIPS (sizeof (tips) / sizeof (tips[0])) gchar * get_tip(gchar *text) { gint i; gboolean found; found = FALSE; for (i = 0; i < NUM_TIPS; i++) { if (strcmp (text, tips[i][0]) == 0) { found = TRUE; break; } } if (!found) return NULL; return g_strdup (tips[i][1]); } GtkWidget * tip_window_new (gchar *tip) { GtkWidget *win; GtkWidget *label; GtkWidget *eb; GdkColormap *cmap; GdkColor color; PangoFontDescription *pfd; win = gtk_window_new (GTK_WINDOW_POPUP); gtk_container_set_border_width (GTK_CONTAINER (win), 0); eb = gtk_event_box_new (); gtk_container_set_border_width (GTK_CONTAINER (eb), 1); gtk_container_add (GTK_CONTAINER (win), eb); label = gtk_label_new (tip); gtk_container_add (GTK_CONTAINER (eb), label); pfd = pango_font_description_from_string ("courier"); gtk_widget_modify_font (label, pfd); cmap = gtk_widget_get_colormap (win); color.red = 0; color.green = 0; color.blue = 0; if (gdk_colormap_alloc_color (cmap, &color, FALSE, TRUE)) gtk_widget_modify_bg (win, GTK_STATE_NORMAL, &color); else g_warning ("Color allocation failed!\n"); cmap = gtk_widget_get_colormap (eb); color.red = 65535; color.green = 65535; color.blue = 45535; if (gdk_colormap_alloc_color (cmap, &color, FALSE, TRUE)) gtk_widget_modify_bg (eb, GTK_STATE_NORMAL, &color); else g_warning ("Color allocation failed!\n"); return win; } /* Called when main window is destroyed. */ void win_destroy (void) { gtk_main_quit(); } void insert_open_brace(GtkWidget **tip_win, GtkWidget *text_view, GtkTextIter *arg1) { GdkWindow *win; GtkTextIter start; GdkRectangle buf_loc; gint x, y; gint win_x, win_y; gchar *text; gchar *tip_text; /* Get the word at cursor. */ start = *arg1; if (!gtk_text_iter_backward_word_start (&start)) return; text = gtk_text_iter_get_text (&start, arg1); g_strstrip (text); /* Get the corresponding tooltip. */ tip_text = get_tip(text); if (tip_text == NULL) return; /* Calculate the tool tip window location. */ gtk_text_view_get_iter_location (GTK_TEXT_VIEW (text_view), arg1, &buf_loc); g_printf ("Buffer: %d, %d\n", buf_loc.x, buf_loc.y); gtk_text_view_buffer_to_window_coords (GTK_TEXT_VIEW (text_view), GTK_TEXT_WINDOW_WIDGET, buf_loc.x, buf_loc.y, &win_x, &win_y); g_printf ("Window: %d, %d\n", win_x, win_y); win = gtk_text_view_get_window (GTK_TEXT_VIEW (text_view), GTK_TEXT_WINDOW_WIDGET); gdk_window_get_origin (win, &x, &y); /* Destroy any previous tool tip window. */ if (*tip_win != NULL) gtk_widget_destroy (GTK_WIDGET (*tip_win)); /* Create a new tool tip window and place it at the caculated location. */ *tip_win = tip_window_new (tip_text); g_free(tip_text); gtk_window_move (GTK_WINDOW (*tip_win), win_x + x, win_y + y + buf_loc.height); gtk_widget_show_all (*tip_win); } void insert_close_brace (GtkWidget **tip_win) { if (*tip_win != NULL) { gtk_widget_destroy (GTK_WIDGET (*tip_win)); *tip_win = NULL; } } void buffer_insert_text (GtkTextBuffer *textbuffer, GtkTextIter *arg1, gchar *arg2, gint arg3, gpointer user_data) { static GtkWidget *tip_win = NULL; if (strcmp (arg2, "(") == 0) { insert_open_brace(&tip_win, GTK_WIDGET (user_data), arg1); } if (strcmp (arg2, ")") == 0) { insert_close_brace(&tip_win); } } int main (int argc, char *argv[]) { GtkWidget *win; GtkWidget *swindow; GtkWidget *text_view; GtkTextBuffer *buffer; PangoFontDescription *pfd; gtk_init (&argc, &argv); /* Create the window. */ win = gtk_window_new (GTK_WINDOW_TOPLEVEL); g_signal_connect (G_OBJECT (win), "destroy", win_destroy, NULL); /* Create the text widget inside a scrolled window. */ swindow = gtk_scrolled_window_new (NULL, NULL); gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (swindow), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); gtk_container_add (GTK_CONTAINER (win), swindow); text_view = gtk_text_view_new (); buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (text_view)); g_signal_connect (G_OBJECT (buffer), "insert_text", G_CALLBACK (buffer_insert_text), text_view); gtk_container_add (GTK_CONTAINER (swindow), text_view); pfd = pango_font_description_from_string ("courier"); gtk_widget_modify_font (text_view, pfd); gtk_widget_show_all (win); gtk_main(); } ------------------------------------------------------------------------------- 7.2. More on Buffer and Window Coordinates In the previous section we obtained the screen coordinates for a position in the buffer. What if we want to do the exact opposite, ie. what if we want to find the position in the buffer corresponding to a particular X-Y coordinate. The GtkTextView has functions for these as well. Window coordinates can be converted to buffer coordinates using void gtk_text_view_window_to_buffer_coords( GtkTextView *text_view, GtkTextWindowType win, gint window_x, gint window_y, gint *buffer_x, gint *buffer_y ); The iter at a buffer coordinate can be obtained using void gtk_text_view_get_iter_at_location( GtkTextView *text_view, GtkTextIter *iter, gint x, gint y ); ------------------------------------------------------------------------------- 8. Final Notes 8.1. GtkTextView in gtk-demo The gtk-demo which is distributed along with the GTK+ library contains a demo on the GtkTextView and friends. It demonstrates some stuff that we have dropped here, like multiple views for a single buffer. It also has a demo on hyperlinking. ------------------------------------------------------------------------------- 8.2. GtkSourceView If you want to display/edit source code, you might consider using GtkSourceView and friends. They extend the GtkTextView widget, adding functionalities like * Indentation * Syntax Highlighting * Bracket Matching * Line numbering GtkSourceView is the widget used by the well known text editor "gedit". The following example is probably the simplest implementation of the GtkSourceView widget. It creates a window with a GtkSourceView, then calls a function to load a C source file from disk and highlight its syntax. [srcview] #include #include #include #include #include static gboolean open_file (GtkSourceBuffer *sBuf, const gchar *filename); int main( int argc, char *argv[] ) { static GtkWidget *window, *pScrollWin, *sView; PangoFontDescription *font_desc; GtkSourceLanguagesManager *lm; GtkSourceBuffer *sBuf; /* Create a Window. */ gtk_init (&argc, &argv); window = gtk_window_new (GTK_WINDOW_TOPLEVEL); g_signal_connect (G_OBJECT(window),"destroy",G_CALLBACK(gtk_main_quit),NULL); gtk_container_set_border_width (GTK_CONTAINER (window), 10); gtk_window_set_default_size (GTK_WINDOW(window), 660, 500); gtk_window_set_position (GTK_WINDOW (window), GTK_WIN_POS_CENTER); /* Create a Scrolled Window that will contain the GtkSourceView */ pScrollWin = gtk_scrolled_window_new (NULL, NULL); gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (pScrollWin), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); /* Now create a GtkSourceLanguagesManager */ lm = gtk_source_languages_manager_new(); /* and a GtkSourceBuffer to hold text (similar to GtkTextBuffer) */ sBuf = GTK_SOURCE_BUFFER (gtk_source_buffer_new (NULL)); g_object_ref (lm); g_object_set_data_full ( G_OBJECT (sBuf), "languages-manager", lm, (GDestroyNotify) g_object_unref); /* Create the GtkSourceView and associate it with the buffer */ sView = gtk_source_view_new_with_buffer(sBuf); /* Set default Font name,size */ font_desc = pango_font_description_from_string ("mono 8"); gtk_widget_modify_font (sView, font_desc); pango_font_description_free (font_desc); /* Attach the GtkSourceView to the scrolled Window */ gtk_container_add (GTK_CONTAINER (pScrollWin), GTK_WIDGET (sView)); /* And the Scrolled Window to the main Window */ gtk_container_add (GTK_CONTAINER (window), pScrollWin); gtk_widget_show_all (pScrollWin); /* Finally load an example file to see how it works */ open_file (sBuf, "srcview.c"); gtk_widget_show (window); gtk_main(); return 0; } static gboolean open_file (GtkSourceBuffer *sBuf, const gchar *filename) { GtkSourceLanguagesManager *lm; GtkSourceLanguage *language = NULL; GError *err = NULL; gboolean reading; GtkTextIter iter; GIOChannel *io; gchar *buffer; g_return_val_if_fail (sBuf != NULL, FALSE); g_return_val_if_fail (filename != NULL, FALSE); g_return_val_if_fail (GTK_IS_SOURCE_BUFFER (sBuf), FALSE); /* get the Language for C source mimetype */ lm = g_object_get_data (G_OBJECT (sBuf), "languages-manager"); language = gtk_source_languages_manager_get_language_from_mime_type (lm, "text/x-csrc"); //g_print("Language: [%s]\n", gtk_source_language_get_name(language)); if (language == NULL) { g_print ("No language found for mime type `%s'\n", "text/x-csrc"); g_object_set (G_OBJECT (sBuf), "highlight", FALSE, NULL); } else { gtk_source_buffer_set_language (sBuf, language); g_object_set (G_OBJECT (sBuf), "highlight", TRUE, NULL); } /* Now load the file from Disk */ io = g_io_channel_new_file (filename, "r", &err); if (!io) { g_print("error: %s %s\n", (err)->message, filename); return FALSE; } if (g_io_channel_set_encoding (io, "utf-8", &err) != G_IO_STATUS_NORMAL) { g_print("err: Failed to set encoding:\n%s\n%s", filename, (err)->message); return FALSE; } gtk_source_buffer_begin_not_undoable_action (sBuf); //gtk_text_buffer_set_text (GTK_TEXT_BUFFER (sBuf), "", 0); buffer = g_malloc (4096); reading = TRUE; while (reading) { gsize bytes_read; GIOStatus status; status = g_io_channel_read_chars (io, buffer, 4096, &bytes_read, &err); switch (status) { case G_IO_STATUS_EOF: reading = FALSE; case G_IO_STATUS_NORMAL: if (bytes_read == 0) continue; gtk_text_buffer_get_end_iter ( GTK_TEXT_BUFFER (sBuf), &iter); gtk_text_buffer_insert (GTK_TEXT_BUFFER(sBuf),&iter,buffer,bytes_read); break; case G_IO_STATUS_AGAIN: continue; case G_IO_STATUS_ERROR: default: g_print("err (%s): %s", filename, (err)->message); /* because of error in input we clear already loaded text */ gtk_text_buffer_set_text (GTK_TEXT_BUFFER (sBuf), "", 0); reading = FALSE; break; } } g_free (buffer); gtk_source_buffer_end_not_undoable_action (sBuf); g_io_channel_unref (io); if (err) { g_error_free (err); return FALSE; } gtk_text_buffer_set_modified (GTK_TEXT_BUFFER (sBuf), FALSE); /* move cursor to the beginning */ gtk_text_buffer_get_start_iter (GTK_TEXT_BUFFER (sBuf), &iter); gtk_text_buffer_place_cursor (GTK_TEXT_BUFFER (sBuf), &iter); g_object_set_data_full (G_OBJECT (sBuf),"filename", g_strdup (filename), (GDestroyNotify) g_free); return TRUE; } To compile it, you have to specify the GtkSourceView library as an argument to pkg-config. $ gcc -Wall -o srcview srcview.c \ > `pkg-config --cflags --libs gtk+-2.0 gtksourceview-1.0` This widget offers several interesting options, here are some of them. void gtk_source_view_set_show_line_numbers (GtkSourceView *view, gboolean show); void gtk_source_view_set_highlight_current_line (GtkSourceView *view, gboolean show); void gtk_source_view_set_auto_indent (GtkSourceView *view, gboolean enable); This is used to show a vertical bar on the right. void gtk_source_view_set_show_margin (GtkSourceView *view, gboolean show); This specifies the column where margin will appear (number of bytes) void gtk_source_view_set_margin (GtkSourceView *view, guint margin); To use them in the example program provided above, you would write: gtk_source_view_set_show_line_numbers (GTK_SOURCE_VIEW(sView), TRUE); gtk_source_view_set_highlight_current_line (GTK_SOURCE_VIEW(sView), TRUE); gtk_source_view_set_auto_indent (GTK_SOURCE_VIEW(sView), TRUE); gtk_source_view_set_show_margin (GTK_SOURCE_VIEW(sView), TRUE); gtk_source_view_set_margin (GTK_SOURCE_VIEW(sView), 80); Furthermore, the GtkSourceview handles several lang files, one for each language. These files are located in /usr/share/gtksourceview-1.0/ language-specs. You can add new files there, specify or add another dir to get lang files, but this goes beyond this tutorial, which simply aims to present this great widget. For more details, see the GtkSourceView manual . ------------------------------------------------------------------------------- 9. Credits A handful of people have contributed to the development of this tutorial. They are listed below in alphabetical order. * Bertrand-Xavier Massot contributed the description and example on GtkSourceView. * Markus Fisher provided suggestions and feedbacks on the tutorial style. * Vijay Kumar wrote the initial tutorial. ------------------------------------------------------------------------------- 10. Tutorial Copyright and Permissions Notice Copyright (C) 2005 Vijay Kumar B. Permission is granted to make and distribute verbatim copies of this manual provided the copyright notice and this permission notice are preserved on all copies. Permission is granted to copy and distribute modified versions of this document under the conditions for verbatim copying, provided that this copyright notice is included exactly as in the original, and that the entire resulting derived work is distributed under the terms of a permission notice identical to this one. Permission is granted to copy and distribute translations of this document into another language, under the above conditions for modified versions. If you are intending to incorporate this document into a published work, please contact the maintainer, and we will make an effort to ensure that you have the most up to date information available. There is no guarantee that this document lives up to its intended purpose. This is simply provided as a free resource. As such, the authors and maintainers of the information provided within can not make any guarantee that the information is even accurate.