Text View#

The Gtk.TextView widget can be used to display and edit large amounts of formatted text. Like the List Widgets, it has a model/view design. In this case the Gtk.TextBuffer is the model which represents the text being edited. This allows two or more Gtk.TextView widgets to share the same Gtk.TextBuffer, and allows those text buffers to be displayed slightly differently. Or you could maintain several text buffers and choose to display each one at different times in the same Gtk.TextView widget.

See also

Text Widget Overview in the GTK documentation.

The View#

The Gtk.TextView is the frontend with which the user can add, edit and delete textual data. They are commonly used to edit multiple lines of text. When creating a Gtk.TextView it contains its own default Gtk.TextBuffer, which is kept in the Gtk.TextView.props.buffer property.

By default, text can be added, edited and removed from the Gtk.TextView. You can disable this by changing Gtk.TextView.props.editable. If the text is not editable, you usually want to hide the text cursor with Gtk.TextView.props.cursor_visible as well. In some cases it may be useful to set the justification of the text with Gtk.TextView.props.justification. The text can be displayed at the left edge, (Gtk.Justification.LEFT), at the right edge (Gtk.Justification.RIGHT), centered (Gtk.Justification.CENTER), or distributed across the complete width (Gtk.Justification.FILL).

Another default setting of the Gtk.TextView widget is long lines of text will continue horizontally until a break is entered. To wrap the text and prevent it going off the edges of the screen set Gtk.TextView.props.wrap_mode similar to Label.

The Model#

The Gtk.TextBuffer is the core of the Gtk.TextView widget, and is used to hold whatever text is being displayed in the Gtk.TextView. Setting and retrieving the contents is possible with Gtk.TextBuffer.props.text. However, most text manipulation is accomplished with iterators, represented by a Gtk.TextIter. An iterator represents a position between two characters in the text buffer. Iterators are not valid indefinitely; whenever the buffer is modified in a way that affects the contents of the buffer, all outstanding iterators become invalid.

Because of this, iterators can’t be used to preserve positions across buffer modifications. To preserve a position, use Gtk.TextMark. A text buffer contains two built-in marks; an “insert” mark (which is the position of the cursor) and the “selection_bound” mark. Both of them can be retrieved using Gtk.TextBuffer.get_insert() and Gtk.TextBuffer.get_selection_bound(), respectively. By default, the location of a Gtk.TextMark is not shown. This can be changed by calling Gtk.TextMark.set_visible().

Many methods exist to retrieve a Gtk.TextIter. For instance, Gtk.TextBuffer.get_start_iter() returns an iterator pointing to the first position in the text buffer, whereas Gtk.TextBuffer.get_end_iter() returns an iterator pointing past the last valid character. Retrieving the bounds of the selected text can be achieved by calling Gtk.TextBuffer.get_selection_bounds().

To insert text at a specific position use Gtk.TextBuffer.insert(). Another useful method is Gtk.TextBuffer.insert_at_cursor() which inserts text wherever the cursor may be currently positioned. To remove portions of the text buffer use Gtk.TextBuffer.delete().

In addition, Gtk.TextIter can be used to locate textual matches in the buffer using Gtk.TextIter.forward_search() and Gtk.TextIter.backward_search(). The start and end iters are used as the starting point of the search and move forwards/backwards depending on requirements.

Tags#

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.TextTag object. One Gtk.TextTag can be applied to any number of text ranges in any number of buffers.

Each tag is stored in a Gtk.TextTagTable. 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.

To specify that some text in the buffer should have specific formatting, you must define a tag to hold that formatting information, and then apply that tag to the region of text using Gtk.TextBuffer.create_tag() and Gtk.TextBuffer.apply_tag():

tag = textbuffer.create_tag('orange_bg', background='orange')
textbuffer.apply_tag(tag, start_iter, end_iter)

The following are some of the common styles applied to text:

  • Background colour (“background” property)

  • Foreground colour (“foreground” property)

  • Underline (“underline” property)

  • Bold (“weight” property)

  • Italics (“style” property)

  • Strikethrough (“strikethrough” property)

  • Justification (“justification” property)

  • Size (“size” and “size-points” properties)

  • Text wrapping (“wrap-mode” property)

You can also delete particular tags later using Gtk.TextBuffer.remove_tag() or delete all tags in a given region by calling Gtk.TextBuffer.remove_all_tags().

Example#

../_images/textview.png
  1import gi
  2
  3gi.require_version('Gtk', '4.0')
  4from gi.repository import Gtk, Pango
  5
  6
  7class SearchDialog(Gtk.Window):
  8    def __init__(self, parent):
  9        super().__init__(title='Search', transient_for=parent)
 10
 11        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
 12        self.set_child(box)
 13
 14        label = Gtk.Label(label='Insert text you want to search for:')
 15        box.append(label)
 16
 17        self.entry = Gtk.Entry()
 18        box.append(self.entry)
 19
 20        self.button = Gtk.Button(label='Find')
 21        box.append(self.button)
 22
 23
 24class TextViewWindow(Gtk.ApplicationWindow):
 25    def __init__(self, **kargs):
 26        super().__init__(**kargs, title='TextView Demo')
 27
 28        self.set_default_size(500, 400)
 29
 30        self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
 31        self.set_child(self.box)
 32
 33        self.create_textview()
 34        self.create_toolbar()
 35        self.create_buttons()
 36
 37    def create_toolbar(self):
 38        toolbar = Gtk.Box(spacing=6)
 39        toolbar.props.margin_top = 6
 40        toolbar.props.margin_start = 6
 41        toolbar.props.margin_end = 6
 42        self.box.prepend(toolbar)
 43
 44        button_bold = Gtk.Button(icon_name='format-text-bold-symbolic')
 45        toolbar.append(button_bold)
 46
 47        button_italic = Gtk.Button(icon_name='format-text-italic-symbolic')
 48        toolbar.append(button_italic)
 49
 50        button_underline = Gtk.Button(
 51            icon_name='format-text-underline-symbolic'
 52        )
 53        toolbar.append(button_underline)
 54
 55        button_bold.connect('clicked', self.on_button_clicked, self.tag_bold)
 56        button_italic.connect(
 57            'clicked', self.on_button_clicked, self.tag_italic
 58        )
 59        button_underline.connect(
 60            'clicked', self.on_button_clicked, self.tag_underline
 61        )
 62
 63        toolbar.append(Gtk.Separator())
 64
 65        justifyleft = Gtk.ToggleButton(
 66            icon_name='format-justify-left-symbolic'
 67        )
 68        toolbar.append(justifyleft)
 69
 70        justifycenter = Gtk.ToggleButton(
 71            icon_name='format-justify-center-symbolic'
 72        )
 73        justifycenter.set_group(justifyleft)
 74        toolbar.append(justifycenter)
 75
 76        justifyright = Gtk.ToggleButton(
 77            icon_name='format-justify-right-symbolic'
 78        )
 79        justifyright.set_group(justifyleft)
 80        toolbar.append(justifyright)
 81
 82        justifyfill = Gtk.ToggleButton(
 83            icon_name='format-justify-fill-symbolic'
 84        )
 85        justifyfill.set_group(justifyleft)
 86        toolbar.append(justifyfill)
 87
 88        justifyleft.connect(
 89            'toggled', self.on_justify_toggled, Gtk.Justification.LEFT
 90        )
 91        justifycenter.connect(
 92            'toggled', self.on_justify_toggled, Gtk.Justification.CENTER
 93        )
 94        justifyright.connect(
 95            'toggled', self.on_justify_toggled, Gtk.Justification.RIGHT
 96        )
 97        justifyfill.connect(
 98            'toggled', self.on_justify_toggled, Gtk.Justification.FILL
 99        )
100
101        toolbar.append(Gtk.Separator())
102
103        button_clear = Gtk.Button(icon_name='edit-clear-symbolic')
104        button_clear.connect('clicked', self.on_clear_clicked)
105        toolbar.append(button_clear)
106
107        toolbar.append(Gtk.Separator())
108
109        button_search = Gtk.Button(icon_name='system-search-symbolic')
110        button_search.connect('clicked', self.on_search_clicked)
111        toolbar.append(button_search)
112
113    def create_textview(self):
114        scrolledwindow = Gtk.ScrolledWindow()
115        scrolledwindow.props.hexpand = True
116        scrolledwindow.props.vexpand = True
117        self.box.append(scrolledwindow)
118
119        self.textview = Gtk.TextView()
120        self.textbuffer = self.textview.get_buffer()
121        self.textbuffer.set_text(
122            'This is some text inside of a Gtk.TextView. '
123            + 'Select text and click one of the buttons "bold", "italic", '
124            + 'or "underline" to modify the text accordingly.'
125        )
126        scrolledwindow.set_child(self.textview)
127
128        self.tag_bold = self.textbuffer.create_tag(
129            'bold', weight=Pango.Weight.BOLD
130        )
131        self.tag_italic = self.textbuffer.create_tag(
132            'italic', style=Pango.Style.ITALIC
133        )
134        self.tag_underline = self.textbuffer.create_tag(
135            'underline', underline=Pango.Underline.SINGLE
136        )
137        self.tag_found = self.textbuffer.create_tag(
138            'found', background='yellow'
139        )
140
141    def create_buttons(self):
142        grid = Gtk.Grid()
143        self.box.append(grid)
144
145        check_editable = Gtk.CheckButton(label='Editable')
146        check_editable.props.active = True
147        check_editable.connect('toggled', self.on_editable_toggled)
148        grid.attach(check_editable, 0, 0, 1, 1)
149
150        check_cursor = Gtk.CheckButton(label='Cursor Visible')
151        check_cursor.props.active = True
152        check_editable.connect('toggled', self.on_cursor_toggled)
153        grid.attach_next_to(
154            check_cursor, check_editable, Gtk.PositionType.RIGHT, 1, 1
155        )
156
157        radio_wrapnone = Gtk.CheckButton(label='No Wrapping')
158        radio_wrapnone.props.active = True
159        grid.attach(radio_wrapnone, 0, 1, 1, 1)
160
161        radio_wrapchar = Gtk.CheckButton(label='Character Wrapping')
162        radio_wrapchar.set_group(radio_wrapnone)
163        grid.attach_next_to(
164            radio_wrapchar, radio_wrapnone, Gtk.PositionType.RIGHT, 1, 1
165        )
166
167        radio_wrapword = Gtk.CheckButton(label='Word Wrapping')
168        radio_wrapword.set_group(radio_wrapnone)
169        grid.attach_next_to(
170            radio_wrapword, radio_wrapchar, Gtk.PositionType.RIGHT, 1, 1
171        )
172
173        radio_wrapnone.connect(
174            'toggled', self.on_wrap_toggled, Gtk.WrapMode.NONE
175        )
176        radio_wrapchar.connect(
177            'toggled', self.on_wrap_toggled, Gtk.WrapMode.CHAR
178        )
179        radio_wrapword.connect(
180            'toggled', self.on_wrap_toggled, Gtk.WrapMode.WORD
181        )
182
183    def on_button_clicked(self, _widget, tag):
184        bounds = self.textbuffer.get_selection_bounds()
185        if len(bounds) != 0:
186            start, end = bounds
187            self.textbuffer.apply_tag(tag, start, end)
188
189    def on_clear_clicked(self, _widget):
190        start = self.textbuffer.get_start_iter()
191        end = self.textbuffer.get_end_iter()
192        self.textbuffer.remove_all_tags(start, end)
193
194    def on_editable_toggled(self, widget):
195        self.textview.props.editable = widget.props.active
196
197    def on_cursor_toggled(self, widget):
198        self.textview.props.cursor_visible = widget.props.active
199
200    def on_wrap_toggled(self, _widget, mode):
201        self.textview.props.wrap_mode = mode
202
203    def on_justify_toggled(self, _widget, justification):
204        self.textview.props.justification = justification
205
206    def on_search_clicked(self, _widget):
207        self.search_dialog = SearchDialog(self)
208        self.search_dialog.button.connect('clicked', self.on_find_clicked)
209        self.search_dialog.present()
210
211    def on_find_clicked(self, _button):
212        cursor_mark = self.textbuffer.get_insert()
213        start = self.textbuffer.get_iter_at_mark(cursor_mark)
214        if start.get_offset() == self.textbuffer.get_char_count():
215            start = self.textbuffer.get_start_iter()
216
217        self.search_and_mark(self.search_dialog.entry.get_text(), start)
218
219    def search_and_mark(self, text, start):
220        end = self.textbuffer.get_end_iter()
221        match = start.forward_search(text, 0, end)
222
223        if match is not None:
224            match_start, match_end = match
225            self.textbuffer.apply_tag(self.tag_found, match_start, match_end)
226            self.search_and_mark(text, match_end)
227
228
229def on_activate(app):
230    win = TextViewWindow(application=app)
231    win.present()
232
233
234app = Gtk.Application(application_id='com.example.App')
235app.connect('activate', on_activate)
236
237app.run(None)