pygtkで遊ぼう(21) gtksourceview(gtk.TextView)で文字をハイライトさせる。

今日はgtksourceviewで文字列をハイライトさせる方法についてです。
この方法はgtk.TextViewにも共通して使えます。


サンプルとして、現在制作中のエディタの検索機能を例にしようと思います。


とりあえず、検索機能のテストも兼ねて、次のような検索バーを付けてみました。
(本当にただ付けました。。といった感じで。。)


検索文字列を入力フィールドに入力すると、次のように、該当する文字列
の状態が変わります。

まず、検索文字入力フィールドに対して'edited'というシグナルハンドラとして
connectします。
この関数は、入力フィールドに文字列が入力されるたびに呼び出されます。


この関数が呼び出されると、以下の処理を行います。

(1) 正規検索モジュールの re を使って文字列を検索します。
(2) reの検索結果をSourceViewのサブクラスに「これハイライトして」と渡す。
(3) カーソル位置に一番近い文字列にカーソルを移動。

    def __on_search_keyword_changed(self, widget, keyword, caseSence):
        '''
        現在表示されているテキスト中をkeywordで検索する。
        
        keyword:
            検索文字列
        caseSence:
            True  -> 大文字、小文字を区別する。
            False -> 大文字、小文字を区別しない。
        '''
        page, editView = self.__get_current_edit_view()
        if editView == None:
            return
        
        if len(keyword) == 0:
            # マークをクリア
            editView.set_highlight(None)
            return

        flag = re.VERBOSE
        if caseSence == False:
            flag |= re.IGNORECASE

        # 検索
        result = re.finditer(re.compile(keyword.decode('utf-8'), flag),
                             editView.get_text().decode('utf-8'))

        print 're complete'
        # 検索結果をハイライトし、現在のカーソル位置の直後のハイライト
        # にカーソルを移動
        editView.set_highlight([r.span() for r in result])
        editView.cursor_to_highlight()

上記の処理、動き的にはインクリメンタルサーチっぽくなります。
インクリメンタルサーチに 正規検索をつかうと大変なことに(ヒットした文字の数が膨大に)なる可能性
があるので、本来こんなことはしないと思いますけど。。reも使ってみたかったので。。(笑
実際、入力フィールドに「\D」とか入力すると。。。検索結果がハイライトされるまでに
長〜〜い時間がかかります。
正規検索自体は一瞬で終わるんですが、文字列をハイライトさせる箇所が膨大なので、
ハイライトデータの設定処理に時間がかかってしまいます。


少し脱線しましたが、次にsourceviewのサブクラス側でハイライトの設定を行っているメソッドです。
sourceviewの文字列をハイライトさせるためには、sourceviewのバッファ(gtksourceview.SourceBuffer)
に対してタグという情報を設定すればOKです。

タグというのは、「ここからここまでの文字を、この属性(色とか)で表示してね」という指定です。

以下のメソッドでは、

(1) それまでにSourceBufferに設定されているタグを削除
(2) 渡された位置情報をもとに、タグ情報を作成してSourceBufferに設定。

を行います。

    def set_highlight(self, posList=None):
        '''
        ハイライトさせるテキスト位置を設定する。
        それまで保持していた内部情報をクリアした後で、
        posListで渡された位置情報をもとに、内部情報を構築する。
        
        (内部情報)
            タプル(タグ情報、テキスト開始イテレータ, テキスト終端イテレータ)
            をリストで管理する。
        posList:
            ハイライトさせるテキストの位置情報のタプル
            [(start, end), (start, end), ....]の形式となっている。
            start-> ハイライトさせる文字列の開始位置(ゼロオリジン)
            end  -> ハイライトさせる文字列の終端位置(ゼロオリジン)
        
        '''
        bf = self.get_buffer()
        if bf == None:
            return

        startIter = bf.get_start_iter()
        endIter = bf.get_end_iter()

        # 現在設定されているタグを削除
        for tag, sIter, eIter in self.highlights:
            bf.remove_tag(tag, startIter, endIter)
        del self.highlights[:]

        if not posList:
            return
        
        # 新しくタグを設定
        for start, end in posList:
            tag = bf.create_tag(foreground='#FFFF00',
                                underline=pango.UNDERLINE_DOUBLE)
            sIter = bf.get_iter_at_offset(start)
            eIter = bf.get_iter_at_offset(end)
            bf.apply_tag(tag, sIter, eIter)
            self.highlights.append((tag, sIter, eIter))

        self.curHighlight = None

さてと。。
上記の方法では、

・検索した結果を全てハイライトさせるため、検索結果が多くなると、検索結果が表示されるまでに時間がかかる。
インクリメンタルサーチにreを使うのはちょっと。。

という問題があるので、これから上記の方法以外の方法(reを使用せずに)に変更してみようと思います。
うまくいったらまたご紹介します。