17. Editor de Texto Multilinha

O widget Gtk.TextView pode ser usado para exibir e editar grandes quantidades de texto formatado. Como o Gtk.TreeView, ele possui um design de modelo/visualização. Neste caso, o Gtk.TextBuffer é o modelo que representa o texto que está sendo editado. Isto permite que dois ou mais widgets Gtk.TextView compartilhem o mesmo Gtk.TextBuffer, e permite que os buffers de texto sejam exibidos de forma ligeiramente diferente. Ou você pode manter vários buffers de texto e optar por exibir cada um deles em momentos diferentes no mesmo widget Gtk.TextView.

17.1. A visão

O Gtk.TextView é o frontend com o qual o usuário pode adicionar, editar e excluir dados textuais. Eles são comumente usados para editar várias linhas de texto. Ao criar um Gtk.TextView ele contém seu próprio padrão Gtk.TextBuffer, que você pode acessar através do método Gtk.TextView.get_buffer().

Por padrão, o texto pode ser adicionado, editado e removido da Gtk.TextView. Você pode desabilitar isso chamando Gtk.TextView.set_editable(). Se o texto não for editável, você geralmente deseja ocultar o cursor de texto com Gtk.TextView.set_cursor_visible() também. Em alguns casos, pode ser útil definir a justificação do texto com Gtk.TextView.set_justification(). O texto pode ser exibido na borda da esquerda, (Gtk.Justification.LEFT), na borda da direita (Gtk.Justification.RIGHT), centralizado (Gtk.Justification.CENTER) ou distribuído em toda a largura (Gtk.Justification.FILL).

Outra configuração padrão do widget Gtk.TextView é que linhas longas de texto continuarão horizontalmente até que uma quebra seja inserida. Para encapsular o texto e impedir que ele saia das bordas da tela, chame Gtk.TextView.set_wrap_mode().

17.2. O modelo

O Gtk.TextBuffer é o núcleo do widget Gtk.TextView e é usado para armazenar qualquer texto que esteja sendo exibido na Gtk.TextView. Definir e recuperar o conteúdo é possível com Gtk.TextBuffer.set_text() e Gtk.TextBuffer.get_text(). No entanto, a maior parte da manipulação de texto é realizada com iteradores, representados por um Gtk.TextIter. Um iterador representa uma posição entre dois caracteres no buffer de texto. Iteradores não são válidos indefinidamente; sempre que o buffer é modificado de uma maneira que afeta o conteúdo do buffer, todos os iteradores pendentes se tornam inválidos.

Por causa disso, os iteradores não podem ser usados para preservar posições nas modificações do buffer. Para preservar uma posição, use Gtk.TextMark. Um buffer de texto contém duas marcas internas; uma marca “insert” (que é a posição do cursor) e a marca “selection_bound”. Ambos podem ser recuperados usando Gtk.TextBuffer.get_insert() e Gtk.TextBuffer.get_selection_bound(), respectivamente. Por padrão, a localização de um Gtk.TextMark não é mostrada. Isso pode ser alterado chamando Gtk.TextMark.set_visible().

Existem muitos métodos para recuperar um Gtk.TextIter. Por exemplo, Gtk.TextBuffer.get_start_iter() retorna um iterador apontando para a primeira posição no buffer de texto, enquanto Gtk.TextBuffer.get_end_iter() retorna um iterador apontando após o último caractere válido. A recuperação dos limites do texto selecionado pode ser obtida chamando Gtk.TextBuffer.get_selection_bounds().

Para inserir texto em uma posição específica use Gtk.TextBuffer.insert(). Outro método útil é Gtk.TextBuffer.insert_at_cursor() que insere texto onde quer que o cursor esteja posicionado no momento. Para remover partes do buffer de texto, use Gtk.TextBuffer.delete().

Além disso, Gtk.TextIter pode ser usado para localizar correspondências textuais no buffer usando Gtk.TextIter.forward_search() e Gtk.TextIter.backward_search(). Os iters inicial e final são usados como ponto de partida da pesquisa e avançam/retrocedem dependendo dos requisitos.

17.3. Tags

O texto em um buffer pode ser marcado com tags. Uma tag é um atributo que pode ser aplicado a um intervalo de texto. Por exemplo, uma tag pode ser chamada de “negrito” e tornar o texto dentro da tag em negrito. No entanto, o conceito de tag é mais geral do que isso; as tags não precisam afetar a aparência. Eles podem afetar o comportamento de pressionamentos de mouse e de tecla, “bloquear” um intervalo de texto para que o usuário não possa editá-lo ou inúmeras outras coisas. Uma tag é representada por um objeto Gtk.TextTag. Um Gtk.TextTag pode ser aplicado a qualquer número de intervalos de texto em qualquer número de buffers.

Cada tag é armazenada em Gtk.TextTagTable. Uma tabela de tags define um conjunto de tags que podem ser usadas juntas. Cada buffer tem uma tabela de tags associada a ele; somente tags dessa tabela de tags podem ser usadas com o buffer. No entanto, uma única tabela de tags pode ser compartilhada entre vários buffers.

Para especificar que algum texto no buffer deve ter uma formatação específica, você deve definir uma tag para manter as informações de formatação e, em seguida, aplicar essa tag à região do texto usando Gtk.TextBuffer.create_tag() e Gtk.TextBuffer.apply_tag():

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

A seguir estão alguns dos estilos comuns aplicados ao texto:

  • Background colour (“background” property)

  • Foreground colour (“foreground” property)

  • Sublinhado (propriedade “underline”)

  • Negrito (propriedade “weight”)

  • Itálico (propriedade “style”)

  • Tachado (propriedade “strikethrough”)

  • Justificação (propriedade de “justification”)

  • Tamanho (propriedades “size” e “size-points”)

  • Quebra automática de texto (propriedade “wrap-mode”)

Você também pode excluir tags particulares posteriormente usando Gtk.TextBuffer.remove_tag() ou excluir todas as tags em uma determinada região chamando Gtk.TextBuffer.remove_all_tags().

17.4. Exemplo

_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()