Texteditor mit GtkSourceView

  |   Source

Text-Widget mit GtkSourceView

Gtk+ bietet mit Gtk.TextView ein Widget zum Anzeigen und Bearbeiten von Text/-dateien an. Wie beim TreeView-Widget werden die Daten (model) und die Anzeige (view) getrennt voneinander gehandhabt. Das datentragende Modell zu TextView ist TextBuffer.

GtkSourceView ist eine Erweiterung und Unterklasse von TextView, die Syntaxhighlighting, Farbschemata, Laden/Speichern, Vervollständigung und andere Funktionen unterstützt.

Im Beispiel wird ein Editor ergestellt, der bestimmte Dateien laden und speichern kann, sowie eine rudimentäre Suchfunktion und ein Widget zum Farbschemawechseln bereitstellt.

/images/22_editor_gtksv.thumbnail.png

Glade

GtkSourceView

Die SourceView-Widgets befinden sich unterhalb einer eigenen gleichnamigen Hauptkategorie in der Seitenleiste.

  • GtkSourceView: das eigentliche Editorwidget, das in einem ScrolledWindow platziert wird
  • GtkSourceMap: Miniaturansicht und Unterklasse von SourceView
  • GtkSourceStyleSchemeChooserWidget: Widget zur Auswahl eines StyleSchemes

In Glade lassen sich bereits viele Eigenschaften des Editorbereichs festlegen wie die Anzeige der Zeilennummern, Einrückung, Umbruchverhalten usw., die sich natürlich auch über set_property festlegen oder ändern lassen.

Beim StyleChooser-Widget wird das Signal button-release-event belegt, um das ausgewählte StyleScheme auf die SourceView-Widgets anzuwenden.

SourceMap

Das Widget muss mit der anzuzeigenden Quelle, einem SourceView-Widget, verknüpft werden (über "Allgemein > View"). Es wird dann der Inhalt des SourceView-Widgets verkleinert (standardmäßig mit Schriftgröße in 1pt) angezeigt. Durch scrollen in SourceMap verändert man gleichzeitig die Anzeige in SourceView.

Headerbar

Die Headerbar enthält verschiedene Buttons zum Laden, Suchen und Speichern:

  • "Python file" und "Glade file" laden die entsprechenden Dateien dieses Beispieles in den Editor (Signal clicked)
  • Die Sucheingabe ist ein Gtk.SearchEntry-Widget (Signale search-changed und activate)
  • "Save .bak" und "Save" speichern die Dateien (Signal clicked)

Python

SourceView

Initialisierung

Widgets, die nicht zum Gtk-Modul gehören, müssen zunächst als initialisiert werden (siehe auch Vte-Terminal):

GObject.type_register(GtkSource.View)

Das SourceView-Widget besitzt bereits einen integrierten Textbuffer, welcher mit get_buffer abgefragt werden kann:

self.buffer = self.view.get_buffer()

Desweiteren werden noch Objekte zum Laden und Speichern von Dateien sowie fürs Syntaxhighlighting benötigt:

self.sourcefile = GtkSource.File()
self.lang_manager = GtkSource.LanguageManager()

Datei laden

Die zu öffnende Datei muss dem GtkSource.File-Objekt im Gio.File-Format und anschließend an GtkSource.FileLoader übergeben werden. Die Information zum Syntaxhighlighting erhält der Buffer:

sourcefile.set_location(Gio.File.new_for_path("file"))
buffer.set_language(self.lang_manager.get_language("language"))
loader = GtkSource.FileLoader.new(buffer, sourcefile)
loader.load_async(0, None, None, None, None, None)

Datei speichern

Analog zum Laden erfolgt das Speichern mit GtkSource.FileSaver. Im Beispiel speichert der "Save"-Button die bestehende Datei (es erfolgt keine "Überschreiben?"-Sicherheitsabfrage) und der "Save .bak"-Button speichert den Inhalt als neue Datei mit genannter Endung ab. Die Übergabe der Dateien erfolgt wie beim Laden Gio.File-formatiert:

#bestehende Datei überschreiben
saver = GtkSource.FileSaver.new(buffer, sourcefile)
#Datei unter anderem Namen speichern
saver = GtkSource.FileSaver.new_with_target(buffer, sourcefile, targetfile)
#Speichern ausführen
saver.save_async(0, None, None, None, None, None)

Text hervorheben

Zunächst ist festzustellen, dass es sich bei den Funktionen suchen(/ersetzen)/markieren und Texthervorhebungen um zwei getrennt voneinander auszuführenden Mechanismen handelt, für die GtkSource.Settings eingerichtet werden müssen:

settings = GtkSource.SearchSettings()
search_context = GtkSource.SearchContext.new(buffer, settings)

Alle Vorkommen eines Strings im TextView lassen sich auf zwei Arten visualisieren, einer naheliegenden und einer eleganten.

Die naheliegende Lösung ist die Ausführung von settings.get_search_text bei der Eingabe von Text in das Suchfeld (Signal search-changed):

Die andere Möglichkeit, bei der kein Signal benötigt wird, ist die direkte Anbindung der SearchSettings-Eigenschaft "search-text" an das Sucheingabefeld:

builder.get_object("search_entry").bind_property('text', settings, 'search-text')

Text markieren

GtkSource.SearchContext wird für die Suchen-/Ersetzen-Funktion innerhalb eines GtkSource.Buffer verwendet. Dieser wurde bereits mit den SearchSettings initialisiert.

Die Markierungsfunktionen und Cursorplatzierung erbt GtkSource.Buffer von Gtk.TextBuffer, die Suche wird mit SeachContexts forward2 ausgeführt.

def find_text(self, start_offset=1):
    buf = self.buffer
    insert = buf.get_iter_at_mark(buf.get_insert())
    start, end = buf.get_bounds()
    insert.forward_chars(start_offset)
    match, start_iter, end_iter, wrapped = self.search_context.forward2(insert)

    if match:
        buf.place_cursor(start_iter)
        buf.move_mark(buf.get_selection_bound(), end_iter)
        self.view.scroll_to_mark(buf.get_insert(), 0.25, True, 0.5, 0.5)
        return True
    else:
        buf.place_cursor(buf.get_iter_at_mark(buf.get_insert()))

Durch die Signalbindung von activate im Suchfeld wird die Suche durch Drücken der Eingabetaste an der letzten Position fortgeführt. Für eine Rückwärtssuche muss analog zu forward2 oder forward_async backward2 oder backward_async verwendet werden.

StyleChooser

Das Widget zeigt die verfügbaren Stile an. Es ist nicht möglich, lokale Stile anzugeben oder sie zu verändern.

Der angewählte Style lässt sich dann einfach auf den gewünschten Buffer anwenden:

def on_signal_emitted(self, widget, event):
    buffer.set_style_scheme(widget.get_style_scheme())
/images/22_editor_gtksv.gif

Listings

Python

22_editor_gtksv.py (Source)

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys
import os

import gi
gi.require_version('Gtk','3.0')
gi.require_version('GtkSource','3.0')

from gi.repository import Gtk, GtkSource, Gio, GObject

class Handler:

    def on_stylechooserwidget_button_release_event(self, widget, event):
        x.buffer.set_style_scheme(widget.get_style_scheme())

    def on_button1_clicked(self, widget):
        x.load_file("22_editor_gtksv.py", "python")

    def on_button2_clicked(self, widget):
        x.load_file("22_editor_gtksv.glade", "xml")

    def on_save_clicked(self, widget):
        saver = GtkSource.FileSaver.new(x.buffer, x.sourcefile)
        saver.save_async(0, None, None, None, None, None)

    def on_saveas_clicked(self, widget):
        saver = GtkSource.FileSaver.new_with_target(x.buffer, x.sourcefile, Gio.File.new_for_path("{}.bak".format(x.file)))
        saver.save_async(0, None, None, None, None, None)

    def on_search_entry_search_changed(self, widget):
        x.find_text(0)

    def on_search_entry_activate(self, widget):
        x.find_text()

class Editor:

    def __init__(self):
        self.app = Gtk.Application.new("org.application.test", Gio.ApplicationFlags(0))
        self.app.connect("activate", self.on_app_activate)

    def on_app_activate(self, app):
        self.builder = Gtk.Builder()
        GObject.type_register(GtkSource.View)
        self.builder.add_from_file("22_editor_gtksv.glade")
        self.builder.connect_signals(Handler())

        #setup SourceView
        self.view = self.builder.get_object("sv")
        self.buffer = self.view.get_buffer()
        self.sourcefile = GtkSource.File()
        self.lang_manager = GtkSource.LanguageManager()

        #setup settings for SourceView
        self.settings = GtkSource.SearchSettings()
        self.builder.get_object("search_entry").bind_property('text', self.settings, 'search-text')
        self.settings.set_search_text("initial highlight")
        self.settings.set_wrap_around(True)
        self.search_context = GtkSource.SearchContext.new(self.buffer, self.settings)

        window = self.builder.get_object("app_window")
        window.set_application(app)
        window.set_wmclass("Tutorial application","Tutorial application")
        window.show_all()

    def run(self, argv):
        self.app.run(argv)

    def load_file(self, f, lang):
        self.file = f
        self.sourcefile.set_location(Gio.File.new_for_path(f))
        self.buffer.set_language(self.lang_manager.get_language(lang))
        loader = GtkSource.FileLoader.new(self.buffer,self.sourcefile)
        loader.load_async(0, None, None, None, None, None)

    def find_text(self, start_offset=1):
        buf = self.buffer
        insert = buf.get_iter_at_mark(buf.get_insert())
        start, end = buf.get_bounds()
        insert.forward_chars(start_offset)
        match, start_iter, end_iter, wrapped = self.search_context.forward2(insert)

        if match:
            buf.place_cursor(start_iter)
            buf.move_mark(buf.get_selection_bound(), end_iter)
            self.view.scroll_to_mark(buf.get_insert(), 0.25, True, 0.5, 0.5)
            return True
        else:
            buf.place_cursor(buf.get_iter_at_mark(buf.get_insert()))

    def main(self):
        Gtk.main()

x = Editor()
x.run(sys.argv)

Glade

22_editor_gtksv.glade (Source)

<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.20.1 -->
<interface>
  <requires lib="gtk+" version="3.20"/>
  <requires lib="gtksourceview" version="3.0"/>
  <object class="GtkApplicationWindow" id="app_window">
    <property name="width_request">600</property>
    <property name="height_request">400</property>
    <property name="can_focus">False</property>
    <child>
      <object class="GtkBox">
        <property name="visible">True</property>
        <property name="can_focus">False</property>
        <property name="hexpand">True</property>
        <property name="vexpand">True</property>
        <child>
          <object class="GtkScrolledWindow">
            <property name="width_request">500</property>
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="shadow_type">in</property>
            <child>
              <object class="GtkSourceView" id="sv">
                <property name="width_request">100</property>
                <property name="height_request">80</property>
                <property name="visible">True</property>
                <property name="can_focus">True</property>
                <property name="wrap_mode">word-char</property>
                <property name="left_margin">2</property>
                <property name="right_margin">2</property>
                <property name="monospace">True</property>
                <property name="show_line_numbers">True</property>
                <property name="tab_width">4</property>
                <property name="insert_spaces_instead_of_tabs">True</property>
                <property name="highlight_current_line">True</property>
              </object>
            </child>
          </object>
          <packing>
            <property name="expand">True</property>
            <property name="fill">True</property>
            <property name="position">0</property>
          </packing>
        </child>
        <child>
          <object class="GtkSourceMap">
            <property name="visible">True</property>
            <property name="can_focus">False</property>
            <property name="editable">False</property>
            <property name="left_margin">2</property>
            <property name="right_margin">2</property>
            <property name="monospace">True</property>
            <property name="view">sv</property>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="fill">False</property>
            <property name="position">1</property>
          </packing>
        </child>
        <child>
          <object class="GtkSourceStyleSchemeChooserWidget" id="stylechooserwidget">
            <property name="visible">True</property>
            <property name="can_focus">False</property>
            <signal name="button-release-event" handler="on_stylechooserwidget_button_release_event" swapped="no"/>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="fill">True</property>
            <property name="position">2</property>
          </packing>
        </child>
      </object>
    </child>
    <child type="titlebar">
      <object class="GtkHeaderBar">
        <property name="visible">True</property>
        <property name="can_focus">False</property>
        <property name="show_close_button">True</property>
        <child>
          <object class="GtkButton" id="button1">
            <property name="label" translatable="yes">Python file</property>
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="receives_default">True</property>
            <signal name="clicked" handler="on_button1_clicked" swapped="no"/>
          </object>
        </child>
        <child>
          <object class="GtkButton" id="button2">
            <property name="label" translatable="yes">Glade file</property>
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="receives_default">True</property>
            <signal name="clicked" handler="on_button2_clicked" swapped="no"/>
          </object>
          <packing>
            <property name="position">1</property>
          </packing>
        </child>
        <child>
          <object class="GtkSearchEntry" id="search_entry">
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="primary_icon_name">edit-find-symbolic</property>
            <property name="primary_icon_activatable">False</property>
            <property name="primary_icon_sensitive">False</property>
            <signal name="activate" handler="on_search_entry_activate" swapped="no"/>
            <signal name="search-changed" handler="on_search_entry_search_changed" swapped="no"/>
          </object>
          <packing>
            <property name="position">4</property>
          </packing>
        </child>
        <child>
          <object class="GtkButton" id="save">
            <property name="label" translatable="yes">Save</property>
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="receives_default">True</property>
            <signal name="clicked" handler="on_save_clicked" swapped="no"/>
          </object>
          <packing>
            <property name="pack_type">end</property>
            <property name="position">2</property>
          </packing>
        </child>
        <child>
          <object class="GtkButton" id="saveas">
            <property name="label" translatable="yes">Save .bak</property>
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="receives_default">True</property>
            <signal name="clicked" handler="on_saveas_clicked" swapped="no"/>
          </object>
          <packing>
            <property name="pack_type">end</property>
            <property name="position">2</property>
          </packing>
        </child>
      </object>
    </child>
  </object>
</interface>

Kommentieren auf
Comments powered by Disqus