[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

9. Multiline Text Editor


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

9.1 Text Widget Overview

GTK+ has an extremely powerful framework for multiline text editing. The primary objects involved in the process are gtk-text-buffer, which represents the text being edited, and gtk-text-view, a widget which can display a gtk-text-buffer. Each buffer can be displayed by any number of views.

One of the important things to remember about text in GTK+ is that it's in the UTF-8 encoding. This means that one character can be encoded as multiple bytes. Character counts are usually referred to as offsets, while byte counts are called indexes. If you confuse these two, things will work fine with ASCII, but as soon as your buffer contains multibyte characters, bad things will happen.

Text in a buffer can be marked with tags. A tag is an attribute that can be applied to some range of text. For example, a tag might be called "bold" and make the text inside the tag bold. However, the tag concept is more general than that; tags don't have to affect appearance. They can instead affect the behavior of mouse and key presses, "lock" a range of text so the user can't edit it, or countless other things. A tag is represented by a gtk-text-tag object. One gtk-text-tag can be applied to any number of text ranges in any number of buffers.

Each tag is stored in a gtk-text-tag-table. A tag table defines a set of tags that can be used together. Each buffer has one tag table associated with it; only tags from that tag table can be used with the buffer. A single tag table can be shared between multiple buffers, however.

Tags can have names, which is convenient sometimes (for example, you can name your tag that makes things bold "bold"), but they can also be anonymous (which is convenient if you're creating tags on-the-fly).

Most text manipulation is accomplished with iterators, represented by a gtk-text-iter. An iterator represents a position between two characters in the text buffer. gtk-text-iter is a struct designed to be allocated on the stack; it is guaranteed to be copiable by value and never contain any heap-allocated data. 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. (Note that deleting 5 characters and then reinserting 5 still invalidates iterators, though you end up with the same number of characters you pass through a state with a different number).

Because of this, iterators can't be used to preserve positions across buffer modifications. To preserve a position, the gtk-text-mark object is ideal. You can think of a mark as an invisible cursor or insertion point; it floats in the buffer, saving a position. If the text surrounding the mark is deleted, the mark remains in the position the text once occupied; if text is inserted at the mark, the mark ends up either to the left or to the right of the new text, depending on its gravity. The standard text cursor in left-to-right languages is a mark with right gravity, because it stays to the right of inserted text.

Like tags, marks can be either named or anonymous. There are two marks built-in to gtk-text-buffer; these are named "insert" and "selection_bound" and refer to the insertion point and the boundary of the selection which is not the insertion point, respectively. If no text is selected, these two marks will be in the same position. You can manipulate what is selected and where the cursor appears by moving these marks around. If you want to place the cursor in response to a user action, be sure to use gtk-text-buffer-place-cursor, which moves both at once without causing a temporary selection (moving one then the other temporarily selects the range in between the old and new positions).

Text buffers always contain at least one line, but may be empty (that is, buffers can contain zero characters). The last line in the text buffer never ends in a line separator (such as newline); the other lines in the buffer always end in a line separator. Line separators count as characters when computing character counts and character offsets. Note that some Unicode line separators are represented with multiple bytes in UTF-8, and the two-character sequence "\r\n" is also considered a line separator.


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

9.2 Simple Multiline Text Widget

figures/simple-text-view302x229

Figure 9.1: Most Simple Text View

The simplest usage of gtk-text-view might look like in example-simple-text-view. The output is shown in figure-simple-text-view.

Example 9.1: Most Simple Text View

(defun example-simple-text-view ()
  (within-main-loop
    (let* ((window (make-instance 'gtk-window
                                  :type :toplevel
                                  :title "Example Simple Text View"
                                  :default-width 300))
           (view (make-instance 'gtk-text-view))
           (buffer (gtk-text-view-buffer view)))
      (g-signal-connect window "destroy"
                        (lambda (widget)
                          (declare (ignore widget))
                          (leave-gtk-main)))
      (gtk-text-buffer-set-text buffer "Hello, this is some text.")
      (gtk-container-add window view)
      (gtk-widget-show-all window))))

In many cases it is also convenient to first create the buffer with gtk-text-buffer-new, then create a widget for that buffer with gtk-text-view-new-with-buffer. Or you can change the buffer the widget displays after the widget is created with gtk-text-view-buffer.


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

9.3 Formatted Text in Text Widget

figures/text-view-attributes352x229

Figure 9.2: Changing Text Attributes of a Text View

There are two ways to affect text attributes in gtk-text-view. You can change the default attributes for a given gtk-text-view, and you can apply tags that change the attributes for a region of text. For text features that come from the theme - such as font and foreground color - use standard gtk-widget functions such as gtk-widget-override-font. For other attributes there are dedicated methods on gtk-text-view such as gtk-text-view-set-tabs.

Example 9.2: Changing Text Attributes of a Text View

(defun example-text-view-attributes ()
  (within-main-loop
    (let* ((window (make-instance 'gtk-window
                                  :type :toplevel
                                  :title "Example Text View Attributes"
                                  :default-width 350))
           (view (make-instance 'gtk-text-view))
           (buffer (gtk-text-view-buffer view)))
      (g-signal-connect window "destroy"
                        (lambda (widget)
                          (declare (ignore widget))
                          (leave-gtk-main)))
      (gtk-text-buffer-set-text buffer "Hello, this is some text.")
      ;; Change default font throughout the widget
      (gtk-widget-override-font
                             view
                             (pango-font-description-from-string "Serif 20"))
      ;; Change default color throughout the widget
      (gtk-widget-override-color view
                                 :normal
                                 (gdk-rgba-parse "red"))
      ;; Change left margin throughout the widget
      (gtk-text-view-set-left-margin view 30)
      ;; Use a tag to change the color for just one part of the widget
      (let ((tag (make-instance 'gtk-text-tag
                                :name "blue_foreground"
                                :foreground "blue"))
            (start (gtk-text-buffer-get-iter-at-offset buffer 7))
            (end (gtk-text-buffer-get-iter-at-offset buffer 12)))
        ;; Add the tag to the tag table of the buffer
        (gtk-text-tag-table-add (gtk-text-buffer-get-tag-table buffer) tag)
        ;; Apply the tag to a region of the text in the buffer
        (gtk-text-buffer-apply-tag buffer tag start end))
      ;; Add the view to the window and show all
      (gtk-container-add window view)
      (gtk-widget-show-all window))))

More about Tags

Tag objects are associated with a buffer and are created using the function gtk-text-buffer-create-tag. 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, nil 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.

See the GTK+ manual http://www.crategus.com/books/cl-cffi-gtk/, for a complete list of properties and their corresponding values.

The created tag can then be applied to a range of text using the functions gtk-text-buffer-apply-tag and gtk-text-buffer-apply-tag-by-name. 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.

figures/text-view-tags

Figure 32: Searching text in a text view.

Example 39: Applying tags.

(defun on-button-clicked (buffer tag)
  (multiple-value-bind (start end)
      (gtk-text-buffer-get-selection-bounds buffer)
    (gtk-text-buffer-apply-tag-by-name buffer tag start end)))

(defun example-text-view-tags ()
  (within-main-loop
    (let* ((window (make-instance 'gtk-window
                                  :title "Multiline Text Input"
                                  :type :toplevel
                                  :default-width 300
                                  :default-height 200))
           (vbox (make-instance 'gtk-grid
                                :orientation :vertical))
           (bbox (make-instance 'gtk-grid
                                :orientation :horizontal))
           (text-view (make-instance 'gtk-text-view
                                     :hexpand t
                                     :vexpand t))
           (buffer (gtk-text-view-buffer text-view)))
      (g-signal-connect window "destroy"
                               (lambda (widget)
                                 (declare (ignore widget))
                                 (leave-gtk-main)))
      (gtk-container-add vbox bbox)
      (gtk-container-add vbox text-view)
      (gtk-text-buffer-set-text buffer "Hello World Text View")
      ;; Create tags associated with the buffer.
      (gtk-text-tag-table-add (gtk-text-buffer-get-tag-table buffer)
                              (make-instance 'gtk-text-tag
                                             :name "bold"
                                             :weight 700)) ; :bold
      (gtk-text-tag-table-add (gtk-text-buffer-get-tag-table buffer)
                              (make-instance 'gtk-text-tag
                                             :name "italic"
                                             :style :italic))
      (gtk-text-tag-table-add (gtk-text-buffer-get-tag-table buffer)
                              (make-instance 'gtk-text-tag
                                             :name "font"
                                             :font "fixed"))
      ;; Create button for bold.
      (let ((button (make-instance 'gtk-button :label "Bold")))
        (g-signal-connect button "clicked"
           (lambda (widget)
             (declare (ignore widget))
             (on-button-clicked buffer "bold")))
        (gtk-container-add bbox button))
      ;; Create button for italic.
      (let ((button (make-instance 'gtk-button :label "Italic")))
        (g-signal-connect button "clicked"
           (lambda (widget)
             (declare (ignore widget))
             (on-button-clicked buffer "italic")))
        (gtk-container-add bbox button))
      ;; Create button for fixed font.
      (let ((button (make-instance 'gtk-button :label "Font Fixed")))
        (g-signal-connect button "clicked"
           (lambda (widget)
             (declare (ignore widget))
             (on-button-clicked buffer "font")))
        (gtk-container-add bbox button))
      ;; Create the close button.
      (let ((button (make-instance 'gtk-button :label "Close")))
        (g-signal-connect button "clicked"
                          (lambda (widget)
                            (declare (ignore widget))
                            (gtk-widget-destroy window)))
        (gtk-container-add vbox button))
      (gtk-container-add window vbox)
      (gtk-widget-show-all window))))

More Functions for Applying and Removing tags

In the previous section, the function gtk-text-buffer-insert was introduced. A variant gtk-text-buffer-insert-with-tags of this function can be used to insert text with tags applied.

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 function gtk-text-buffer-remove-tag.

This function also has the _by_name prefixed variant. All tags on a range of text can be removed in one go using the function gtk-text-buffer-remove-all-tags.

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 gtk-text-view widget, the gtk-text-view-set-* functions can be used. For example, the function gtk-text-view-set-editable makes text-view editable/non-editable.

See the GTK+ API documentation http://www.crategus.com/books/cl-cffi-gtk/ 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.


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

9.4 Cut, Copy and Paste


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

9.5 Searching

The functions gtk-text-iter-forward-search and gtk-text-iter-backward-search with the arguments iter, str, flags, limit can be used to search for a given text within a buffer. Both functions return as the first value a boolean to indicate wether the search was sucessful. If this is the case the second and third values contain the iterators match-start and match-end.

The function gtk-text-iter-forward-search searches for str starting from iter in the forward direction. The start and end iterators of the first matched string are return as the values match-start and match-end. The search is limited to the iterator limit, if specified. The function returns nil, if no 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.

In the Lips binding to GTK+ we have in addition the function gtk-text-iter-search which combines the functions for forward and backward search and handles the arguments flags and limit as keyword arguments. In addition the keyword argument direction with a default value of :forward indicates the direction of the search. Set the value of direction to :backward for backward search.

The function gtk-text-buffer-get-selection-bounds was introduced earlier, to obtain the iterators around the current selection. To set the current selection programmatically the function gtk-text-buffer-select-range with the arguments buffer, start, end can be used. 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.

figures/text-view-search

Figure 9.1: Searching text in a text view.

Example 9.1: Searching text in a text view.

(defvar *some-text*
        "One of the important things to remember about text in GTK+ is that
it is in the UTF-8 encoding. This means that one character can be encoded as
multiple bytes. Character counts are usually referred to as offsets, while
byte counts are called indexes. If you confuse these two, things will work
fine with ASCII, but as soon as your buffer contains multibyte characters,
bad things will happen.")

(defun example-text-view-search ()
  (within-main-loop
    (let ((window (make-instance 'gtk-window
                                 :title "Example Text View Search"
                                 :type :toplevel
                                 :default-width 300
                                 :default-height 200))
          (entry (make-instance 'gtk-entry))
          (button (make-instance 'gtk-button
                                 :label "Search"))
          (scrolled (make-instance 'gtk-scrolled-window))
          (text-view (make-instance 'gtk-text-view
                                    :wrap-mode :word
                                    :hexpand t
                                    :vexpand t))
          (vbox (make-instance 'gtk-grid
                               :orientation :vertical))
          (hbox (make-instance 'gtk-grid
                               :orientation :horizontal)))
      (g-signal-connect window "destroy"
                        (lambda (widget)
                          (declare (ignore widget))
                          (leave-gtk-main)))
      ;; Signal handler for the search button
      (g-signal-connect button "clicked"
         (lambda (widget)
           (declare (ignore widget))
           (let* ((text (gtk-entry-text entry))
                  (buffer (gtk-text-view-buffer text-view))
                  (iter (gtk-text-buffer-get-start-iter buffer)))
             (multiple-value-bind (found start end)
                 (gtk-text-iter-search iter text)
               (when found
                 (gtk-text-buffer-select-range buffer start end))))))
      (gtk-text-buffer-set-text (gtk-text-view-buffer text-view)
                                *some-text*)
      (gtk-container-add scrolled text-view)
      (gtk-container-add hbox entry)
      (gtk-container-add hbox button)
      (gtk-container-add vbox hbox)
      (gtk-container-add vbox scrolled)
      (gtk-container-add window vbox)
      (gtk-widget-show-all window))))

Continuing the search with Marks

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 the function gtk-text-iter-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.

A mark preserves a position in the buffer between modifications. This is possible because their behavior is defined when text is inserted or deleted. 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 function gtk-text-buffer-create-mark with the arguments buffer, mark-name, where and left-grafity can be used to create a mark associated with a buffer.

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 argument 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 function gtk-text-buffer-get-mark

With named tags, you do not have to carry around a pointer to the marker, which can be easily retrieved using the function 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 function gtk-text-buffer-get-iter-at-mark with the arguments buffer and mark returns the iter at the position of mark.

The Scrolling Problem

Before we show an example, we have to solve the problem that the text view should scroll to the matched text. It can be irritating when the matched text is not in the visible portion of the buffer. The function gtk-text-view-scroll-mark-onscreen with the arguments text-view and mark scrolls to a position in the buffer. The argument mark specifies the position to scroll to. Note that this is a method of the gtk-text-view widget rather than a gtk-text-buffer object. Since it does not change the contents of the buffer, it only changes the way a buffer is viewed.

The following example shows the usage of marks to continue the search.

figures/text-view-find-next

Figure 34: Searching text in a text view.

Example 41: Searching text in a text view.

(defun find-text (text-view text iter)
  (let ((buffer (gtk-text-view-buffer text-view)))
    (multiple-value-bind (found start end)
        (gtk-text-iter-search iter text)
      (when found
        (gtk-text-buffer-select-range buffer start end)
        (let ((last-pos (gtk-text-buffer-create-mark buffer "last-pos" end)))
          (gtk-text-view-scroll-mark-onscreen text-view last-pos))))))

(defun example-text-view-find-next ()
  (within-main-loop
    (let ((window (make-instance 'gtk-window
                                 :title "Multiline Text Search"
                                 :type :toplevel
                                 :default-width 450
                                 :default-height 200))
          (entry (make-instance 'gtk-entry))
          (button-search (make-instance 'gtk-button
                                        :label "Search"))
          (button-next (make-instance 'gtk-button
                                      :label "Next"))
          (scrolled (make-instance 'gtk-scrolled-window))
          (text-view (make-instance 'gtk-text-view
                                    :hexpand t
                                    :vexpand t))
          (vbox (make-instance 'gtk-grid
                               :orientation :vertical))
          (hbox (make-instance 'gtk-grid
                               :orientation :horizontal)))
      (g-signal-connect window "destroy"
                        (lambda (widget)
                          (declare (ignore widget))
                          (leave-gtk-main)))
      (g-signal-connect button-search "clicked"
         (lambda (widget)
           (declare (ignore widget))
           (let* ((text (gtk-entry-text entry))
                  (buffer (gtk-text-view-buffer text-view))
                  (iter (gtk-text-buffer-get-start-iter buffer)))
             (find-text text-view text iter))))
      (g-signal-connect button-next "clicked"
         (lambda (widget)
           (declare (ignore widget))
           (let* ((text (gtk-entry-text entry))
                  (buffer (gtk-text-view-buffer text-view))
                  (last-pos (gtk-text-buffer-get-mark buffer "last-pos")))
             (when last-pos
               (find-text text-view
                          text
                          (gtk-text-buffer-get-iter-at-mark buffer
                                                            last-pos))))))
      (gtk-text-buffer-set-text (gtk-text-view-buffer text-view)
                                *some-text*)
      (gtk-container-add scrolled text-view)
      (gtk-container-add hbox entry)
      (gtk-container-add hbox button-search)
      (gtk-container-add hbox button-next)
      (gtk-container-add vbox hbox)
      (gtk-container-add vbox scrolled)
      (gtk-container-add window vbox)
      (gtk-widget-show-all window))))

More on Marks

When a mark is no longer required, it can be deleted using the functions gtk-text-buffer-delete-mark or gtk-text-buffer-delete-mark-by-name. There are two marks built-in to gtk-text-buffer - "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.


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

9.6 Examing and Modify Text

Examining and modifying text is another common operation performed on text buffers. Examples are 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 gtk-text-iter 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 the function gtk-text-iter-set-line-offset with the arguments iter and 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 the function gtk-text-iter-forward-to-line-end. Now that we know the functions required to implement the first program, here is the code.

figures/text-view-editing-1

Figure 9.1

Example 9.1: Modifiy text in a text view.

(defun example-text-editing-text-1 ()
  (within-main-loop
    (let ((window (make-instance 'gtk-window
                                 :title "Multiline Text Editing"
                                 :type :toplevel
                                 :default-width 300
                                 :default-height 200))
          (text-view (make-instance 'gtk-text-view
                                    :hexpand t
                                    :vexpand t))
          (button (make-instance 'gtk-button
                                 :label "Make List Item"))
          (vbox (make-instance 'gtk-grid
                                :orientation :vertical)))
    (g-signal-connect window "destroy"
                      (lambda (widget)
                        (declare (ignore widget))
                        (leave-gtk-main)))
    (g-signal-connect button "clicked"
       (lambda (widget)
         (declare (ignore widget))
         (let* ((buffer (gtk-text-view-buffer text-view))
                (cursor (gtk-text-buffer-get-mark buffer "insert"))
                (iter (gtk-text-buffer-get-iter-at-mark buffer cursor)))
           (gtk-text-iter-set-line-offset iter 0)
           (gtk-text-buffer-insert buffer "<li>" :position iter)
           (gtk-text-iter-forward-to-line-end iter)
           (gtk-text-buffer-insert buffer "</li>" :position iter))))
   (gtk-text-buffer-set-text (gtk-text-view-buffer text-view)
                             (format nil "Item 1~%Item 2~%Item 3~%"))
   (gtk-container-add vbox text-view)
   (gtk-container-add vbox button)
   (gtk-container-add window vbox)
   (gtk-widget-show-all window))))

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 the function gtk-text-iter-backward-find-char or the function gtk-text-iter-find-char with a value :backward for the keyword argument direction.

The function proceeds backwards from iter, and calls pred for each character in the buffer, with the character as argument, till pred returns true. 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 not nil) and returns nil. 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 function gtk-text-iter-forward-char can be used.

To extract the character at an iter the function gtk-text-iter-get-char can be used. After determining the tag type we do the following,

We have not 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 the function gtk-text-iter-backward-find-char. The code for the second example follows.

figures/text-view-editing-2

Figure 9.2

Example 9.2: Modifiy text in a text view.

(defun get-this-tag (iter buffer)
  (let* ((start-tag (gtk-text-iter-copy iter))
         end-tag)
    (and (gtk-text-iter-find-char start-tag #'alpha-char-p)
         (setq end-tag (gtk-text-iter-copy start-tag))
         (gtk-text-iter-find-char end-tag
                                  (lambda (ch) (not (alphanumericp ch))))
         (gtk-text-buffer-get-text buffer start-tag end-tag nil))))

(defun closing-tag-p (iter)
  (let ((slash (gtk-text-iter-copy iter)))
    (gtk-text-iter-forward-char slash)
    (eql (gtk-text-iter-get-char slash) #\/)))

(defun example-text-view-editing-2 ()
  (within-main-loop
    (let ((window (make-instance 'gtk-window
                                 :title "Multiline Editing Text"
                                 :type :toplevel
                                 :default-width 300
                                 :defalut-height 200))
          (text-view (make-instance 'gtk-text-view
                                    :hexpand t
                                    :vexpand t))
          (button (make-instance 'gtk-button
                                 :label "Insert Close Tag"))
          (vbox (make-instance 'gtk-grid
                               :orientation :vertical)))
      (g-signal-connect window "destroy"
                        (lambda (widget)
                          (declare (ignore widget))
                          (leave-gtk-main)))
      (g-signal-connect button "clicked"
         (lambda (widget)
           (declare (ignore widget))
           (let* ((buffer (gtk-text-view-buffer text-view))
                  (cursor (gtk-text-buffer-get-mark buffer "insert"))
                  (iter (gtk-text-buffer-get-iter-at-mark buffer cursor)))

             (do ((stack '()))
                 ((not (gtk-text-iter-find-char iter
                                               (lambda (ch) (eq ch #\<))
                                               :direction :backward)))
               (let ((tag (get-this-tag iter buffer)))
                 (if (closing-tag-p iter)
                     (push tag stack)
                     (let ((tag-in-stack (pop stack)))
                       (when (not tag-in-stack)
                         (gtk-text-buffer-insert buffer
                                                 (format nil "</~a>" tag))
                         (return)))))))))
      (gtk-text-buffer-set-text (gtk-text-view-buffer text-view)
                                (format nil
                                        "<html>~%~
                                         <head><title>Title</title></head>~%~
                                         <body>~%~
                                         <h1>Heading</h1>~%"))
      (gtk-container-add vbox text-view)
      (gtk-container-add vbox button)
      (gtk-container-add window vbox)
      (gtk-widget-show-all window))))

[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

9.7 Images and Widgets

Inserting Images

A text buffer can hold images and anchor location for widgets. An image can be inserted into a buffer using the function gtk-text-buffer-insert-pixbuf with the arguments buffer, iter, and pixbuf. An image represented by pixbuf is inserted at iter. The pixbuf can be created from an image file using the function gdk-pixbuf-new-from-file. See the API documentation for gdk-pixbuf for more details.

The example program given below takes in an image filename and inserts the corresponding image into a buffer.

figures/text-view-insert-image

Figure 37

Example 44: Insert an image.

(defun example-text-view-insert-image ()
  (within-main-loop
    (let ((window (make-instance 'gtk-window
                                 :type :toplevel
                                 :title "Multiline Text Widget"
                                 :default-width 300
                                 :default-height 200))
          (text-view (make-instance 'gtk-text-view
                                    :hexpand t
                                    :vexpand t))
          (button (make-instance 'gtk-button
                                 :label "Insert Image"))
          (vbox (make-instance 'gtk-grid
                               :orientation :vertical)))
    (g-signal-connect window "destroy"
                      (lambda (widget)
                        (declare (ignore widget))
                        (leave-gtk-main)))
    ;; Signal handler to insert an image at the current cursor position.
    (g-signal-connect button "clicked"
       (lambda (widget)
         (declare (ignore widget))
         (let* ((pixbuf (gdk-pixbuf-new-from-file "save.png"))
                (buffer (gtk-text-view-buffer text-view))
                (cursor (gtk-text-buffer-get-insert buffer))
                (iter (gtk-text-buffer-get-iter-at-mark buffer cursor)))
           (gtk-text-buffer-insert-pixbuf buffer iter pixbuf))))
    (gtk-container-add vbox text-view)
    (gtk-container-add vbox button)
    (gtk-container-add window vbox)
    (gtk-widget-show-all window))))

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 the function 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 the function gtk-text-iter-get-pixbuf.

Inserting Widgets

Inserting a widget, unlike inserting an image, is a two step process. The additional complexity is due to the functionality split between gtk-text-view and gtk-text-buffer. The first step is to create and insert a gtk-text-child-anchor. A widget is held in a buffer using a gtk-text-child-anchor. 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 the function gtk-text-buffer-create-child-anchor with the arguments buffer and 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 with the function gtk-text-view-add-child-at-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 gtk-text-view objects. The following program inserts a button widget into a text buffer, whenever the user clicks on the Insert button.

figures/text-view-insert-widget

Figure 38

Example 45: Insert a widget.

(defun example-text-view-insert-widget ()
  (within-main-loop
    (let ((window (make-instance 'gtk-window
                                 :type :toplevel
                                 :title "Multiline Text Widget"
                                 :default-width 300
                                 :default-height 200))
          (text-view (make-instance 'gtk-text-view
                                    :hexpand t
                                    :vexpand t))
          (button (make-instance 'gtk-button
                                 :label "Insert Widget"))
          (vbox (make-instance 'gtk-grid
                               :orientation :vertical)))
    (g-signal-connect window "destroy"
                      (lambda (widget)
                        (declare (ignore widget))
                        (leave-gtk-main)))
    ;; Signal handler to insert a widget at the current cursor position.
    (g-signal-connect button "clicked"
       (lambda (widget)
         (declare (ignore widget))
         (let* ((buffer (gtk-text-view-buffer text-view))
                (cursor (gtk-text-buffer-get-insert buffer))
                (iter (gtk-text-buffer-get-iter-at-mark buffer 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 text-view button anchor)
           (gtk-widget-show button))))
    (gtk-container-add vbox text-view)
    (gtk-container-add vbox button)
    (gtk-container-add window vbox)
    (gtk-widget-show-all window))))

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 the function gtk-text-iter-get-child-anchor. Next, the widget(s) associated with the child anchor has to be retrieved. This can be done using the function gtk-text-child-anchor-get-widgets. 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.


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

9.8 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.

figures/buffercoord

Figure 9.1

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).

In this section, you will learn how to display tooltips under the text cursor. The procedure is as follows,

The buffer coordinates of a particular character in a buffer can be obtained using the function gtk-text-view-get-iter-at-location with the argument iter. The function gets the rectangle that contains the character at iter and returns it. The x and y members of location gives us the buffer coordinates.

The buffer coordinates can be converted into window coordinates using the function gtk-text-view-buffer-to-window-coords. The function converts buffer coordinates (buffer-x, buffer-y), to window coordinates (window-x, window-y).

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 gdk-window associated with it. Once we know the gdk-window associated with a widget, we can obtain it's X-Y coordinates using gdk-window-get-origin. The gdk-window of the text view widget can be obtained using the function gtk-text-view-get-window. Here again you will have to pass :widget for the argument 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 lambda (buffer pos text length).

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.

figures/text-view-tooltip

Figure 9.2

Example 9.1: Show tooltips in a Text View.

(let ((tooltip nil))
  (defun get-tip (word)
    (cdr (assoc word
                '(("printf" . "(const char *format, ...)")
                  ("fprintf" . "(FILE *stream, const char *format, ...)")
                  ("sprintf" . "(char *str, 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)"))
                :test #'equal)))

  (defun tip-window-new (tip-text)
    (let ((win (make-instance 'gtk-window
                              :type :popup
                              :border-width 0))
          (event-box (make-instance 'gtk-event-box
                                    :border-width 1))
          (label (make-instance 'gtk-label
                                :label tip-text)))
      (gtk-widget-override-font
          label
          (pango-font-description-from-string "Courier"))
      (gtk-widget-override-background-color win
                                            :normal
                                            (gdk-rgba-parse "Black"))
      (gtk-widget-override-color win :normal (gdk-rgba-parse "Blue"))
      (gtk-container-add event-box label)
      (gtk-container-add win event-box)
      win))

  (defun insert-open-brace (window text-view location)
    (let ((start (gtk-text-iter-copy location)))
      (when (gtk-text-iter-backward-word-start start)
        (let* ((word (string-trim " "
                                  (gtk-text-iter-get-text start location)))
               (tip-text (get-tip word)))
          (when tip-text
            (let ((rect (gtk-text-view-get-iter-location text-view location))
                  (win (gtk-text-view-get-window text-view :widget)))
              (multiple-value-bind (win-x win-y)
                  (gtk-text-view-buffer-to-window-coords
                      text-view
                      :widget
                      (gdk-rectangle-x rect)
                      (gdk-rectangle-y rect))
                (multiple-value-bind (x y)
                    (gdk-window-get-origin win)
                  ;; Destroy any previous tool tip window
                  (when tooltip
                    (gtk-widget-destroy tooltip)
                    (setf tooltip nil))
                  ;; Create a new tool tip window
                  (setf tooltip (tip-window-new tip-text))
                  ;; Place it at the calculated position.
                  (gtk-window-move tooltip
                                   (+ win-x x)
                                   (+ win-y y (gdk-rectangle-height rect)))
                  (gtk-widget-show-all tooltip)))))))))

  (defun example-text-view-tooltip ()
    (within-main-loop
      (let* ((window (make-instance 'gtk-window
                                    :title "Multiline Text Search"
                                    :type :toplevel
                                    :default-width 450
                                    :default-height 200))
             (scrolled (make-instance 'gtk-scrolled-window))
             (text-view (make-instance 'gtk-text-view
                                       :hexpand t
                                       :vexpand t))
             (buffer (gtk-text-view-buffer text-view)))
        ;; Signal handler for the window
        (g-signal-connect window "destroy"
                        (lambda (widget)
                          (declare (ignore widget))
                          (when tooltip
                            (gtk-widget-destroy tooltip)
                            (setf tooltip nil))
                          (leave-gtk-main)))
        ;; Signal handler for the buffer of the text view
        (g-signal-connect buffer "insert-text"
           (lambda (buffer location text len)
             (declare (ignore buffer len))
             (when (equal text "(")
               (insert-open-brace window text-view location))
             (when (equal text ")")
               (when tooltip
                 (gtk-widget-destroy tooltip)
                 (setf tooltip nil)))))
        ;; Change the default font
        (gtk-widget-override-font
            text-view
            (pango-font-description-from-string "Courier 12"))
        ;; Add the widgets to window and show all
        (gtk-container-add scrolled text-view)
        (gtk-container-add window scrolled)
        (gtk-widget-show-all window)))))

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, i. e. what if we want to find the position in the buffer corresponding to a particular X-Y coordinate. The gtk-text-view has functions for these as well.

Window coordinates can be converted to buffer coordinates using the function gtk-text-view-window-to-buffer-coords.

The iter at a buffer coordinate can be obtained using the function gtk-text-view-get-iter-at-location.


[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [Top] [Contents] [Index] [ ? ]

9.9 Final Notes


[ << ] [ >> ]           [Top] [Contents] [Index] [ ? ]

This document was generated by Crategus on January, 10 2016 using texi2html 1.76.