17. Textredigerare med flera rader

Komponenten Gtk.TextView kan användas för att visa och redigera stora mängder av formaterad text. Som Gtk.TreeView har den en model/view-design. I detta fall är Gtk.TextBuffer modellen som representerar texten som redigeras. Detta låter två eller fler Gtk.TextView-komponenter dela på samma Gtk.TextBuffer, och låter dessa textbuffrar visas något annorlunda. Eller så kan du underhålla flera textbuffrar och välja att visa var och en vid olika tider i samma Gtk.TextView-komponent.

17.1. Vyn

Gtk.TextView är framänden med vilken användaren kan lägga till, redigera och ta bort textdata. De används vanligen för att redigera flera rader av text. Då du skapar en Gtk.TextView innehåller den sin egen standard-Gtk.TextBuffer, vilken du kan komma åt via metoden Gtk.TextView.get_buffer().

Som standard kan text läggas till, redigeras och tas bort från Gtk.TextView. Du kan inaktivera detta genom att anropa Gtk.TextView.set_editable(). Om texten inte är redigerbar vill du vanligen även dölja textmarkören med Gtk.TextView.set_cursor_visible(). I vissa fall kan det vara användbart att ställa in justeringen av texten med Gtk.TextView.set_justification(). Texten kan visas i vänstra kanten, (Gtk.Justification.LEFT), i högra kanten (Gtk.Justification.RIGHT), centrerad (Gtk.Justification.CENTER) eller jämnt fördelad över hela bredden (Gtk.Justification.FILL).

En annan standardinställning för Gtk.TextView-komponenten är att långa textrader kommer fortsätta horisontellt till ett avbrott matas in. För att radbryta texten och förhindra den från att försvinna utanför skärmens kanter, anropa Gtk.TextView.set_wrap_mode().

17.2. Modellen

Gtk.TextBuffer är kärnan av Gtk.TextView-komponenten, och används för att hålla vadhelst för text som visas i Gtk.TextView. Att ställa in och erhålla innehållet är möjligt med Gtk.TextBuffer.set_text() och Gtk.TextBuffer.get_text(). Den mesta textmanipuleringen utförs dock med iteratorer, representerade av en Gtk.TextIter. En iterator representerar en position mellan två tecken i textbufferten. Iteratorer är inte giltiga för alltid, närhelst bufferten ändras på ett sätt som påverkar buffertens innehåll så blir alla tidigare iteratorer ogiltiga.

På grund av detta kan iteratorer inte användas för att bevara positioner över buffertändringar. För att bevara en position, använd Gtk.TextMark. En textbuffert innehåller två inbyggda märken; ett ”insert”-märke (som är markörens position) och märket ”selection_bound”. Båda av dem kan erhållas med Gtk.TextBuffer.get_insert() respektive Gtk.TextBuffer.get_selection_bound(). Som standard visas inte platsen för en Gtk.TextMark. Detta kan ändras genom att anropa Gtk.TextMark.set_visible().

Många metoder finns för att erhålla en Gtk.TextIter. Exempelvis returnerar Gtk.TextBuffer.get_start_iter() en iterator som pekar på den första positionen i textbufferten, medan Gtk.TextBuffer.get_end_iter() returnerar en iterator som pekar bortom det sista giltiga tecknet. Att erhålla gränserna för den markerade texten kan åstadkommas genom att anropa Gtk.TextBuffer.get_selection_bounds().

För att infoga text på en specifik position, använd Gtk.TextBuffer.insert(). En annan användbar metod är Gtk.TextBuffer.insert_at_cursor() som infogar text varhelst markören är placerad för tillfället. Använd Gtk.TextBuffer.delete() för att ta bort delar av textbufferten.

Dessutom kan Gtk.TextIter användas för att hitta sökträffar på text i bufferten med Gtk.TextIter.forward_search() och Gtk.TextIter.backward_search(). Start- och slutiteratorerna används som startpunkten för sökningen och går framåt/bakåt beroende på kraven.

17.3. Taggar

Text i en buffert kan markeras med taggar. En tagg är ett attribut som kan tillämpas på något textintervall. Exempelvis kan en tagg vara kallad ”bold” och få texten i taggen att vara i fetstil. Taggkonceptet är dock mer allmänt än så; taggar behöver inte påverka utseende. De kan istället påverka beteendet för mus- och tangentryck, ”låsa” ett textintervall så att användaren inte kan redigera det samt många andra saker. En tagg representeras av ett Gtk.TextTag-objekt. En Gtk.TextTag kan tillämpas på ett valfritt antal textintervall i ett valfritt antal buffertar.

Varje tagg lagras i en Gtk.TextTagTable. En taggtabell definierar en uppsättning taggar som kan användas tillsammans. Varje buffert har en taggtabell associerad med sig; endast taggar från den taggtabellen kan användas med bufferten. En enda taggtabell kan dock delas mellan flera buffertar.

För att ange att någon text i bufferten ska ha specifik formatering så måste du definiera en tagg för att hålla den formateringsinformationen, och sedan tillämpa den taggen på textregionen med Gtk.TextBuffer.create_tag() och Gtk.TextBuffer.apply_tag():

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

Följande är några av de vanliga stilarna som tillämpas på text:

  • Bakgrundsfärg (egenskapen ”background”)

  • Förgrundsfärg (egenskapen ”foreground”)

  • Understruken (egenskapen ”underline”)

  • Fet (egenskapen ”weight”)

  • Kursiv (egenskapen ”style”)

  • Genomstruken (egenskapen ”strikethrough”)

  • Justering (egenskapen ”justification”)

  • Storlek (egenskaperna ”size” och ”size-points”)

  • Radbrytning av text (egenskapen ”wrap-mode”)

Du kan också ta bort specifika taggar senare med Gtk.TextBuffer.remove_tag() eller ta bort alla taggar i en angiven region genom att anropa Gtk.TextBuffer.remove_all_tags().

17.4. Exempel

_images/textview_example.png
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
import gi

gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, Pango


class SearchDialog(Gtk.Dialog):
    def __init__(self, parent):
        super().__init__(title="Search", transient_for=parent, modal=True)
        self.add_buttons(
            Gtk.STOCK_FIND,
            Gtk.ResponseType.OK,
            Gtk.STOCK_CANCEL,
            Gtk.ResponseType.CANCEL,
        )

        box = self.get_content_area()

        label = Gtk.Label(label="Insert text you want to search for:")
        box.add(label)

        self.entry = Gtk.Entry()
        box.add(self.entry)

        self.show_all()


class TextViewWindow(Gtk.Window):
    def __init__(self):
        Gtk.Window.__init__(self, title="TextView Example")

        self.set_default_size(-1, 350)

        self.grid = Gtk.Grid()
        self.add(self.grid)

        self.create_textview()
        self.create_toolbar()
        self.create_buttons()

    def create_toolbar(self):
        toolbar = Gtk.Toolbar()
        self.grid.attach(toolbar, 0, 0, 3, 1)

        button_bold = Gtk.ToolButton()
        button_bold.set_icon_name("format-text-bold-symbolic")
        toolbar.insert(button_bold, 0)

        button_italic = Gtk.ToolButton()
        button_italic.set_icon_name("format-text-italic-symbolic")
        toolbar.insert(button_italic, 1)

        button_underline = Gtk.ToolButton()
        button_underline.set_icon_name("format-text-underline-symbolic")
        toolbar.insert(button_underline, 2)

        button_bold.connect("clicked", self.on_button_clicked, self.tag_bold)
        button_italic.connect("clicked", self.on_button_clicked, self.tag_italic)
        button_underline.connect("clicked", self.on_button_clicked, self.tag_underline)

        toolbar.insert(Gtk.SeparatorToolItem(), 3)

        radio_justifyleft = Gtk.RadioToolButton()
        radio_justifyleft.set_icon_name("format-justify-left-symbolic")
        toolbar.insert(radio_justifyleft, 4)

        radio_justifycenter = Gtk.RadioToolButton.new_from_widget(radio_justifyleft)
        radio_justifycenter.set_icon_name("format-justify-center-symbolic")
        toolbar.insert(radio_justifycenter, 5)

        radio_justifyright = Gtk.RadioToolButton.new_from_widget(radio_justifyleft)
        radio_justifyright.set_icon_name("format-justify-right-symbolic")
        toolbar.insert(radio_justifyright, 6)

        radio_justifyfill = Gtk.RadioToolButton.new_from_widget(radio_justifyleft)
        radio_justifyfill.set_icon_name("format-justify-fill-symbolic")
        toolbar.insert(radio_justifyfill, 7)

        radio_justifyleft.connect(
            "toggled", self.on_justify_toggled, Gtk.Justification.LEFT
        )
        radio_justifycenter.connect(
            "toggled", self.on_justify_toggled, Gtk.Justification.CENTER
        )
        radio_justifyright.connect(
            "toggled", self.on_justify_toggled, Gtk.Justification.RIGHT
        )
        radio_justifyfill.connect(
            "toggled", self.on_justify_toggled, Gtk.Justification.FILL
        )

        toolbar.insert(Gtk.SeparatorToolItem(), 8)

        button_clear = Gtk.ToolButton()
        button_clear.set_icon_name("edit-clear-symbolic")
        button_clear.connect("clicked", self.on_clear_clicked)
        toolbar.insert(button_clear, 9)

        toolbar.insert(Gtk.SeparatorToolItem(), 10)

        button_search = Gtk.ToolButton()
        button_search.set_icon_name("system-search-symbolic")
        button_search.connect("clicked", self.on_search_clicked)
        toolbar.insert(button_search, 11)

    def create_textview(self):
        scrolledwindow = Gtk.ScrolledWindow()
        scrolledwindow.set_hexpand(True)
        scrolledwindow.set_vexpand(True)
        self.grid.attach(scrolledwindow, 0, 1, 3, 1)

        self.textview = Gtk.TextView()
        self.textbuffer = self.textview.get_buffer()
        self.textbuffer.set_text(
            "This is some text inside of a Gtk.TextView. "
            + "Select text and click one of the buttons 'bold', 'italic', "
            + "or 'underline' to modify the text accordingly."
        )
        scrolledwindow.add(self.textview)

        self.tag_bold = self.textbuffer.create_tag("bold", weight=Pango.Weight.BOLD)
        self.tag_italic = self.textbuffer.create_tag("italic", style=Pango.Style.ITALIC)
        self.tag_underline = self.textbuffer.create_tag(
            "underline", underline=Pango.Underline.SINGLE
        )
        self.tag_found = self.textbuffer.create_tag("found", background="yellow")

    def create_buttons(self):
        check_editable = Gtk.CheckButton(label="Editable")
        check_editable.set_active(True)
        check_editable.connect("toggled", self.on_editable_toggled)
        self.grid.attach(check_editable, 0, 2, 1, 1)

        check_cursor = Gtk.CheckButton(label="Cursor Visible")
        check_cursor.set_active(True)
        check_editable.connect("toggled", self.on_cursor_toggled)
        self.grid.attach_next_to(
            check_cursor, check_editable, Gtk.PositionType.RIGHT, 1, 1
        )

        radio_wrapnone = Gtk.RadioButton.new_with_label_from_widget(None, "No Wrapping")
        self.grid.attach(radio_wrapnone, 0, 3, 1, 1)

        radio_wrapchar = Gtk.RadioButton.new_with_label_from_widget(
            radio_wrapnone, "Character Wrapping"
        )
        self.grid.attach_next_to(
            radio_wrapchar, radio_wrapnone, Gtk.PositionType.RIGHT, 1, 1
        )

        radio_wrapword = Gtk.RadioButton.new_with_label_from_widget(
            radio_wrapnone, "Word Wrapping"
        )
        self.grid.attach_next_to(
            radio_wrapword, radio_wrapchar, Gtk.PositionType.RIGHT, 1, 1
        )

        radio_wrapnone.connect("toggled", self.on_wrap_toggled, Gtk.WrapMode.NONE)
        radio_wrapchar.connect("toggled", self.on_wrap_toggled, Gtk.WrapMode.CHAR)
        radio_wrapword.connect("toggled", self.on_wrap_toggled, Gtk.WrapMode.WORD)

    def on_button_clicked(self, widget, tag):
        bounds = self.textbuffer.get_selection_bounds()
        if len(bounds) != 0:
            start, end = bounds
            self.textbuffer.apply_tag(tag, start, end)

    def on_clear_clicked(self, widget):
        start = self.textbuffer.get_start_iter()
        end = self.textbuffer.get_end_iter()
        self.textbuffer.remove_all_tags(start, end)

    def on_editable_toggled(self, widget):
        self.textview.set_editable(widget.get_active())

    def on_cursor_toggled(self, widget):
        self.textview.set_cursor_visible(widget.get_active())

    def on_wrap_toggled(self, widget, mode):
        self.textview.set_wrap_mode(mode)

    def on_justify_toggled(self, widget, justification):
        self.textview.set_justification(justification)

    def on_search_clicked(self, widget):
        dialog = SearchDialog(self)
        response = dialog.run()
        if response == Gtk.ResponseType.OK:
            cursor_mark = self.textbuffer.get_insert()
            start = self.textbuffer.get_iter_at_mark(cursor_mark)
            if start.get_offset() == self.textbuffer.get_char_count():
                start = self.textbuffer.get_start_iter()

            self.search_and_mark(dialog.entry.get_text(), start)

        dialog.destroy()

    def search_and_mark(self, text, start):
        end = self.textbuffer.get_end_iter()
        match = start.forward_search(text, 0, end)

        if match is not None:
            match_start, match_end = match
            self.textbuffer.apply_tag(self.tag_found, match_start, match_end)
            self.search_and_mark(text, match_end)


win = TextViewWindow()
win.connect("destroy", Gtk.main_quit)
win.show_all()
Gtk.main()