17. Multiline Text Editor¶
The Gtk.TextView
widget can be used to display and edit large amounts
of formatted text. Like the Gtk.TreeView
, 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.
17.1. 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 you can access via the Gtk.TextView.get_buffer()
method.
By default, text can be added, edited and removed from the Gtk.TextView
.
You can disable this by calling Gtk.TextView.set_editable()
.
If the text is not editable, you usually want to hide the text cursor with
Gtk.TextView.set_cursor_visible()
as well. In some cases it may be useful
to set the justification of the text with Gtk.TextView.set_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 call Gtk.TextView.set_wrap_mode()
.
17.2. Das Modell¶
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.set_text()
and Gtk.TextBuffer.get_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.
17.4. Beispiel¶
1import gi
2
3gi.require_version("Gtk", "3.0")
4from gi.repository import Gtk, Pango
5
6
7class SearchDialog(Gtk.Dialog):
8 def __init__(self, parent):
9 Gtk.Dialog.__init__(
10 self, title="Search", transient_for=parent, modal=True,
11 )
12 self.add_buttons(
13 Gtk.STOCK_FIND,
14 Gtk.ResponseType.OK,
15 Gtk.STOCK_CANCEL,
16 Gtk.ResponseType.CANCEL,
17 )
18
19 box = self.get_content_area()
20
21 label = Gtk.Label(label="Insert text you want to search for:")
22 box.add(label)
23
24 self.entry = Gtk.Entry()
25 box.add(self.entry)
26
27 self.show_all()
28
29
30class TextViewWindow(Gtk.Window):
31 def __init__(self):
32 Gtk.Window.__init__(self, title="TextView Example")
33
34 self.set_default_size(-1, 350)
35
36 self.grid = Gtk.Grid()
37 self.add(self.grid)
38
39 self.create_textview()
40 self.create_toolbar()
41 self.create_buttons()
42
43 def create_toolbar(self):
44 toolbar = Gtk.Toolbar()
45 self.grid.attach(toolbar, 0, 0, 3, 1)
46
47 button_bold = Gtk.ToolButton()
48 button_bold.set_icon_name("format-text-bold-symbolic")
49 toolbar.insert(button_bold, 0)
50
51 button_italic = Gtk.ToolButton()
52 button_italic.set_icon_name("format-text-italic-symbolic")
53 toolbar.insert(button_italic, 1)
54
55 button_underline = Gtk.ToolButton()
56 button_underline.set_icon_name("format-text-underline-symbolic")
57 toolbar.insert(button_underline, 2)
58
59 button_bold.connect("clicked", self.on_button_clicked, self.tag_bold)
60 button_italic.connect("clicked", self.on_button_clicked, self.tag_italic)
61 button_underline.connect("clicked", self.on_button_clicked, self.tag_underline)
62
63 toolbar.insert(Gtk.SeparatorToolItem(), 3)
64
65 radio_justifyleft = Gtk.RadioToolButton()
66 radio_justifyleft.set_icon_name("format-justify-left-symbolic")
67 toolbar.insert(radio_justifyleft, 4)
68
69 radio_justifycenter = Gtk.RadioToolButton.new_from_widget(radio_justifyleft)
70 radio_justifycenter.set_icon_name("format-justify-center-symbolic")
71 toolbar.insert(radio_justifycenter, 5)
72
73 radio_justifyright = Gtk.RadioToolButton.new_from_widget(radio_justifyleft)
74 radio_justifyright.set_icon_name("format-justify-right-symbolic")
75 toolbar.insert(radio_justifyright, 6)
76
77 radio_justifyfill = Gtk.RadioToolButton.new_from_widget(radio_justifyleft)
78 radio_justifyfill.set_icon_name("format-justify-fill-symbolic")
79 toolbar.insert(radio_justifyfill, 7)
80
81 radio_justifyleft.connect(
82 "toggled", self.on_justify_toggled, Gtk.Justification.LEFT
83 )
84 radio_justifycenter.connect(
85 "toggled", self.on_justify_toggled, Gtk.Justification.CENTER
86 )
87 radio_justifyright.connect(
88 "toggled", self.on_justify_toggled, Gtk.Justification.RIGHT
89 )
90 radio_justifyfill.connect(
91 "toggled", self.on_justify_toggled, Gtk.Justification.FILL
92 )
93
94 toolbar.insert(Gtk.SeparatorToolItem(), 8)
95
96 button_clear = Gtk.ToolButton()
97 button_clear.set_icon_name("edit-clear-symbolic")
98 button_clear.connect("clicked", self.on_clear_clicked)
99 toolbar.insert(button_clear, 9)
100
101 toolbar.insert(Gtk.SeparatorToolItem(), 10)
102
103 button_search = Gtk.ToolButton()
104 button_search.set_icon_name("system-search-symbolic")
105 button_search.connect("clicked", self.on_search_clicked)
106 toolbar.insert(button_search, 11)
107
108 def create_textview(self):
109 scrolledwindow = Gtk.ScrolledWindow()
110 scrolledwindow.set_hexpand(True)
111 scrolledwindow.set_vexpand(True)
112 self.grid.attach(scrolledwindow, 0, 1, 3, 1)
113
114 self.textview = Gtk.TextView()
115 self.textbuffer = self.textview.get_buffer()
116 self.textbuffer.set_text(
117 "This is some text inside of a Gtk.TextView. "
118 + "Select text and click one of the buttons 'bold', 'italic', "
119 + "or 'underline' to modify the text accordingly."
120 )
121 scrolledwindow.add(self.textview)
122
123 self.tag_bold = self.textbuffer.create_tag("bold", weight=Pango.Weight.BOLD)
124 self.tag_italic = self.textbuffer.create_tag("italic", style=Pango.Style.ITALIC)
125 self.tag_underline = self.textbuffer.create_tag(
126 "underline", underline=Pango.Underline.SINGLE
127 )
128 self.tag_found = self.textbuffer.create_tag("found", background="yellow")
129
130 def create_buttons(self):
131 check_editable = Gtk.CheckButton(label="Editable")
132 check_editable.set_active(True)
133 check_editable.connect("toggled", self.on_editable_toggled)
134 self.grid.attach(check_editable, 0, 2, 1, 1)
135
136 check_cursor = Gtk.CheckButton(label="Cursor Visible")
137 check_cursor.set_active(True)
138 check_editable.connect("toggled", self.on_cursor_toggled)
139 self.grid.attach_next_to(
140 check_cursor, check_editable, Gtk.PositionType.RIGHT, 1, 1
141 )
142
143 radio_wrapnone = Gtk.RadioButton.new_with_label_from_widget(None, "No Wrapping")
144 self.grid.attach(radio_wrapnone, 0, 3, 1, 1)
145
146 radio_wrapchar = Gtk.RadioButton.new_with_label_from_widget(
147 radio_wrapnone, "Character Wrapping"
148 )
149 self.grid.attach_next_to(
150 radio_wrapchar, radio_wrapnone, Gtk.PositionType.RIGHT, 1, 1
151 )
152
153 radio_wrapword = Gtk.RadioButton.new_with_label_from_widget(
154 radio_wrapnone, "Word Wrapping"
155 )
156 self.grid.attach_next_to(
157 radio_wrapword, radio_wrapchar, Gtk.PositionType.RIGHT, 1, 1
158 )
159
160 radio_wrapnone.connect("toggled", self.on_wrap_toggled, Gtk.WrapMode.NONE)
161 radio_wrapchar.connect("toggled", self.on_wrap_toggled, Gtk.WrapMode.CHAR)
162 radio_wrapword.connect("toggled", self.on_wrap_toggled, Gtk.WrapMode.WORD)
163
164 def on_button_clicked(self, widget, tag):
165 bounds = self.textbuffer.get_selection_bounds()
166 if len(bounds) != 0:
167 start, end = bounds
168 self.textbuffer.apply_tag(tag, start, end)
169
170 def on_clear_clicked(self, widget):
171 start = self.textbuffer.get_start_iter()
172 end = self.textbuffer.get_end_iter()
173 self.textbuffer.remove_all_tags(start, end)
174
175 def on_editable_toggled(self, widget):
176 self.textview.set_editable(widget.get_active())
177
178 def on_cursor_toggled(self, widget):
179 self.textview.set_cursor_visible(widget.get_active())
180
181 def on_wrap_toggled(self, widget, mode):
182 self.textview.set_wrap_mode(mode)
183
184 def on_justify_toggled(self, widget, justification):
185 self.textview.set_justification(justification)
186
187 def on_search_clicked(self, widget):
188 dialog = SearchDialog(self)
189 response = dialog.run()
190 if response == Gtk.ResponseType.OK:
191 cursor_mark = self.textbuffer.get_insert()
192 start = self.textbuffer.get_iter_at_mark(cursor_mark)
193 if start.get_offset() == self.textbuffer.get_char_count():
194 start = self.textbuffer.get_start_iter()
195
196 self.search_and_mark(dialog.entry.get_text(), start)
197
198 dialog.destroy()
199
200 def search_and_mark(self, text, start):
201 end = self.textbuffer.get_end_iter()
202 match = start.forward_search(text, 0, end)
203
204 if match is not None:
205 match_start, match_end = match
206 self.textbuffer.apply_tag(self.tag_found, match_start, match_end)
207 self.search_and_mark(text, match_end)
208
209
210win = TextViewWindow()
211win.connect("destroy", Gtk.main_quit)
212win.show_all()
213Gtk.main()