Python GTK+3 チュートリアル

16. 複数行テキスト・エディタ

翻訳して勉強するGtkチュートリアル第16章 Multiline Text Editor です。文字起こしツールのメイン部分はこのウィジェットになりそうです。


Gtk.TextView ウィジェットは、大量のフォーマットされたテキストを表示したり編集したりするのに使えます。Gtk.TreeView と同様に、モデル/ビューのデザインを持っています。この場合、Gtk.TextBuffer は編集されるテキストを表すモデルです。これにより、2つ以上の Gtk.TextView ウィジェットが同じ Gtk.TextBuffer を共有することができ、それらのテキストバッファを微妙に異なる表示にすることができます。あるいは、複数のテキストバッファを保持し、同じ Gtk.TextView ウィジェットの中でそれぞれを異なる時間に表示することもできます。

16.1. ビュー

Gtk.TextView は、ユーザがテキストデータを追加、編集、削除するためのフロントエンドです。Gtk.TextView は複数行のテキストを編集するためによく使われます。Gtk.TextView を作成する際には、それ自身のデフォルトの Gtk.TextBuffer が含まれており、Gtk.TextView.get_buffer() メソッドでアクセスすることができます。

デフォルトでは、テキストは Gtk.TextView から追加、編集、削除ができます。これを無効にするには Gtk.TextView.set_editable() を呼び出す必要があります。テキストが編集できない場合、通常は Gtk.TextView.set_cursor_visible() でテキストカーソルを非表示にしたいでしょう。場合によっては、Gtk.TextView.set_justification() でテキストの位置揃えを設定しておくと便利かもしれません。テキストは左端 (Gtk.Justification.LEFT)、右端 (Gtk.Justification.RIGHT)、中央 (Gtk.Justification.CENTER)、あるいは全幅 (Gtk.Justification.FILL) に表示することができます。

Gtk.TextView ウィジェットのもう一つのデフォルト設定では、長いテキストの行は改行が入るまで水平方向に続きます。テキストを折り返して画面の端から外れないようにするには Gtk.TextView.set_wrap_mode() を呼び出してください。

16.2. モデル

Gtk.TextBuffer Gtk.TextView ウィジェットの中核であり、Gtk.TextView に表示されているテキストを保持するために使用されます。コンテンツの設定や取得は Gtk.TextBuffer.set_text() Gtk.TextBuffer.get_text() で可能です。しかし、ほとんどのテキスト操作は Gtk.TextIter で表されるイテレータを使って行われます。イテレータはテキストバッファ内の二文字間の位置を表します。イテレータは無期限に有効ではありません。バッファの内容に影響を与えるような方法でバッファが変更された場合はいつでも、未解決のイテレータはすべて無効になります。

このため、バッファの変更にまたがって位置を保持するためにはイテレータは使用できません。位置を保持するには Gtk.TextMark を使用します。テキストバッファには “insert” マーク (カーソルの位置) と “slection_bound” マークという2つのマークが組み込まれています。両方とも、それぞれ Gtk.TextBuffer.get_insert() Gtk.TextBuffer.get_selection_bound() を使って取得することができます。デフォルトでは、Gtk.TextMark の位置は表示されません。これは Gtk.TextMark.set_visible() を呼び出すことで変更できます。

Gtk.TextIter を取得するためのメソッドは数多く存在します。例えば、Gtk.TextBuffer.get_start_iter() はテキストバッファの最初の位置を指すイテレータを返し、Gtk.TextBuffer.get_end_iter() は最後の有効な文字を指すイテレータを返します。選択されたテキストの境界の取得は Gtk.TextBuffer.get_selection_bounds() を呼び出すことで可能です。

特定の位置にテキストを挿入するには Gtk.TextBuffer.insert() を使用します。もう一つの便利なメソッドは Gtk.TextBuffer.insert_at_cursor() で、カーソルが現在位置している場所ならどこにでもテキストを挿入することができます。テキストバッファの一部を削除するには Gtk.TextBuffer.delete() を使用します。

さらに、Gtk.TextIter は、Gtk.TextIter.forward_search() Gtk.TextIter.backward_search() を使用して、バッファ内のテキストマッチの位置を特定するために使用できます。開始イターと終了イターは検索の開始点として使用され、要件に応じて前方/後方に移動します。

16.3. タグ

バッファ内のテキストはタグでマークすることができます。タグとは、ある範囲のテキストに適用できる属性のことです。例えば、“bold” と呼ばれるタグは、タグ内のテキストを太字にします。しかし、タグの概念はそれよりももっと一般的です。タグは、マウスやキーの押し方に影響を与えたり、テキストの範囲を「ロック」してユーザが編集できないようにしたり、その他数え切れないほどのことができます。タグは Gtk.TextTag オブジェクトで表現されます。1つの Gtk.TextTag は、任意の数のバッファ内の、任意の数のテキスト範囲に適用することができます。

各タグは Gtk.TextTagTable に格納されています。タグテーブルは、一緒に使用できるタグのセットを定義します。各バッファには1つのタグテーブルが関連付けられており、そのタグテーブルのタグのみがバッファで使用できます。しかし、1つのタグテーブルは複数のバッファ間で共有することができます。

バッファ内のテキストの一部に特定の書式を指定するには、その書式情報を保持するタグを定義し、Gtk.TextBuffer.create_tag() Gtk.TextBuffer.apply_tag() を使用してそのタグをテキストの領域に適用しなければなりません。

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

テキストに適用される一般的なスタイルは以下の通りです。

  • Background colour (“foreground” property)
  • Foreground colour (“background” 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)

また、後で Gtk.TextBuffer.remove_tag() を使って特定のタグを削除したり、Gtk.TextBuffer.remove_all_tags() を呼び出して指定された領域のすべてのタグを削除することもできます。

16.4. 例

動作テスト - Multiline Text Editor
# tut16.py
# マルチライン・テキストエディタ

import gi

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


class SearchDialog(Gtk.Dialog):
    def __init__(self, parent):
        Gtk.Dialog.__init__(
            self,
            "Search",
            parent,
            Gtk.DialogFlags.MODAL,
            buttons=(
                Gtk.STOCK_FIND,
                Gtk.ResponseType.OK,
                Gtk.STOCK_CANCEL,
                Gtk.ResponseType.CANCEL,
            ),
        )

        box = self.get_content_area()

        label = Gtk.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("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("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()


関連記事