Text View#
The Gtk.TextView widget can be used to display and edit large amounts
of formatted text. Like the Gtk.ListView, 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.
Example#
 
  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(icon_name="format-text-underline-symbolic")
 51        toolbar.append(button_underline)
 52
 53        button_bold.connect("clicked", self.on_button_clicked, self.tag_bold)
 54        button_italic.connect("clicked", self.on_button_clicked, self.tag_italic)
 55        button_underline.connect("clicked", self.on_button_clicked, self.tag_underline)
 56
 57        toolbar.append(Gtk.Separator())
 58
 59        justifyleft = Gtk.ToggleButton(icon_name="format-justify-left-symbolic")
 60        toolbar.append(justifyleft)
 61
 62        justifycenter = Gtk.ToggleButton(icon_name="format-justify-center-symbolic")
 63        justifycenter.set_group(justifyleft)
 64        toolbar.append(justifycenter)
 65
 66        justifyright = Gtk.ToggleButton(icon_name="format-justify-right-symbolic")
 67        justifyright.set_group(justifyleft)
 68        toolbar.append(justifyright)
 69
 70        justifyfill = Gtk.ToggleButton(icon_name="format-justify-fill-symbolic")
 71        justifyfill.set_group(justifyleft)
 72        toolbar.append(justifyfill)
 73
 74        justifyleft.connect("toggled", self.on_justify_toggled, Gtk.Justification.LEFT)
 75        justifycenter.connect(
 76            "toggled", self.on_justify_toggled, Gtk.Justification.CENTER
 77        )
 78        justifyright.connect(
 79            "toggled", self.on_justify_toggled, Gtk.Justification.RIGHT
 80        )
 81        justifyfill.connect("toggled", self.on_justify_toggled, Gtk.Justification.FILL)
 82
 83        toolbar.append(Gtk.Separator())
 84
 85        button_clear = Gtk.Button(icon_name="edit-clear-symbolic")
 86        button_clear.connect("clicked", self.on_clear_clicked)
 87        toolbar.append(button_clear)
 88
 89        toolbar.append(Gtk.Separator())
 90
 91        button_search = Gtk.Button(icon_name="system-search-symbolic")
 92        button_search.connect("clicked", self.on_search_clicked)
 93        toolbar.append(button_search)
 94
 95    def create_textview(self):
 96        scrolledwindow = Gtk.ScrolledWindow()
 97        scrolledwindow.props.hexpand = True
 98        scrolledwindow.props.vexpand = True
 99        self.box.append(scrolledwindow)
100
101        self.textview = Gtk.TextView()
102        self.textbuffer = self.textview.get_buffer()
103        self.textbuffer.set_text(
104            "This is some text inside of a Gtk.TextView. "
105            + 'Select text and click one of the buttons "bold", "italic", '
106            + 'or "underline" to modify the text accordingly.'
107        )
108        scrolledwindow.set_child(self.textview)
109
110        self.tag_bold = self.textbuffer.create_tag("bold", weight=Pango.Weight.BOLD)
111        self.tag_italic = self.textbuffer.create_tag("italic", style=Pango.Style.ITALIC)
112        self.tag_underline = self.textbuffer.create_tag(
113            "underline", underline=Pango.Underline.SINGLE
114        )
115        self.tag_found = self.textbuffer.create_tag("found", background="yellow")
116
117    def create_buttons(self):
118        grid = Gtk.Grid()
119        self.box.append(grid)
120
121        check_editable = Gtk.CheckButton(label="Editable")
122        check_editable.props.active = True
123        check_editable.connect("toggled", self.on_editable_toggled)
124        grid.attach(check_editable, 0, 0, 1, 1)
125
126        check_cursor = Gtk.CheckButton(label="Cursor Visible")
127        check_cursor.props.active = True
128        check_editable.connect("toggled", self.on_cursor_toggled)
129        grid.attach_next_to(check_cursor, check_editable, Gtk.PositionType.RIGHT, 1, 1)
130
131        radio_wrapnone = Gtk.CheckButton(label="No Wrapping")
132        radio_wrapnone.props.active = True
133        grid.attach(radio_wrapnone, 0, 1, 1, 1)
134
135        radio_wrapchar = Gtk.CheckButton(label="Character Wrapping")
136        radio_wrapchar.set_group(radio_wrapnone)
137        grid.attach_next_to(
138            radio_wrapchar, radio_wrapnone, Gtk.PositionType.RIGHT, 1, 1
139        )
140
141        radio_wrapword = Gtk.CheckButton(label="Word Wrapping")
142        radio_wrapword.set_group(radio_wrapnone)
143        grid.attach_next_to(
144            radio_wrapword, radio_wrapchar, Gtk.PositionType.RIGHT, 1, 1
145        )
146
147        radio_wrapnone.connect("toggled", self.on_wrap_toggled, Gtk.WrapMode.NONE)
148        radio_wrapchar.connect("toggled", self.on_wrap_toggled, Gtk.WrapMode.CHAR)
149        radio_wrapword.connect("toggled", self.on_wrap_toggled, Gtk.WrapMode.WORD)
150
151    def on_button_clicked(self, _widget, tag):
152        bounds = self.textbuffer.get_selection_bounds()
153        if len(bounds) != 0:
154            start, end = bounds
155            self.textbuffer.apply_tag(tag, start, end)
156
157    def on_clear_clicked(self, _widget):
158        start = self.textbuffer.get_start_iter()
159        end = self.textbuffer.get_end_iter()
160        self.textbuffer.remove_all_tags(start, end)
161
162    def on_editable_toggled(self, widget):
163        self.textview.props.editable = widget.props.active
164
165    def on_cursor_toggled(self, widget):
166        self.textview.props.cursor_visible = widget.props.active
167
168    def on_wrap_toggled(self, _widget, mode):
169        self.textview.props.wrap_mode = mode
170
171    def on_justify_toggled(self, _widget, justification):
172        self.textview.props.justification = justification
173
174    def on_search_clicked(self, _widget):
175        self.search_dialog = SearchDialog(self)
176        self.search_dialog.button.connect("clicked", self.on_find_clicked)
177        self.search_dialog.present()
178
179    def on_find_clicked(self, _button):
180        cursor_mark = self.textbuffer.get_insert()
181        start = self.textbuffer.get_iter_at_mark(cursor_mark)
182        if start.get_offset() == self.textbuffer.get_char_count():
183            start = self.textbuffer.get_start_iter()
184
185        self.search_and_mark(self.search_dialog.entry.get_text(), start)
186
187    def search_and_mark(self, text, start):
188        end = self.textbuffer.get_end_iter()
189        match = start.forward_search(text, 0, end)
190
191        if match is not None:
192            match_start, match_end = match
193            self.textbuffer.apply_tag(self.tag_found, match_start, match_end)
194            self.search_and_mark(text, match_end)
195
196
197def on_activate(app):
198    win = TextViewWindow(application=app)
199    win.present()
200
201
202app = Gtk.Application(application_id="com.example.App")
203app.connect("activate", on_activate)
204
205app.run(None)