Texteditor mit GtkSourceView

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

Das Geheimnis der Sphinx

Ob ich die Tutorial-Artikel nicht als E-Book zusammenfassen könnte, wurde ich gefragt. Ich kann.

Folgt mir auf meiner abenteuerlichen und actiongeladenen Reise, in dem mit Kanonen auf Spatzen geschossen wird und ich den Geheimnissen der rätselhaften Sphinx auf die Spur komme.

/images/sph_cover.thumbnail.jpeg

Vorüberlegungen

Die GitHub-Page läuft mit dem statischen Seitengenerator Nikola, die standardmäßig reStructuredText-Quelldateien parst. Da kommt ein Dokumentationstool, das diese ebenso verarbeiten kann und die Ausgabe in verschiedene Formate ermöglicht, absolut gelegen. All das bietet Sphinx.

Die naheliegende, sich aber möglicherweise als naiv herausstellende, Überlegung war nun, die bestehenden Blogartikelquelldateien so vorzubereiten, dass sich mit wenig Aufwand gewünschte Ausgabeformate immer wieder neu generieren lassen.

Sphinx bietet Builder für

  • EPUB, dem offenen E-Book-Standard, nativ von allen E-Reader-Fabrikaten außer den Kindles unterstützt
  • PDF, das per LaTeX (verschiedene Engines verfügbar) erzeugt wird

Sphinx

Initialisierung

Nach der Installation erstellt man das Projektverzeichnis und initialisiert mit

$ sphinx-quickstart

das Grundgerüst. Fast alle Fragen können auf der Voreinstellung belassen werden. Im Projektverzeichnis befindet sich nun die Konfigurationsdatei conf.py sowie das Root-Dokument index.rst.

Die Dateien lassen sich nach dem Muster make Builder erzeugen, die in den Unterverzeichnissen _build/builder befinden:

$ make epub
$ make latexpdf

conf.py

Epub

#HTML-Dateien vor dem Inhalt der index.rst einfügen
epub_pre_files = [('info.xhtml', 'Info')]
html_additional_pages = {'info': 'info.html'}

#Titel erzeugen
epub_cover = ('_static/cover.png', 'epub-cover.html')

#Stichwortverzeichnis auslassen
epub_use_index = False

#Bezeichnung der Ausgabedatei
epub_basename = output_basename

#für die Generierung der info.xhtml benötigt, da sonst None
html_last_updated_fmt = '%d. %B %Y'

LaTeX

Für die PDF-Ausgabe müssen eine Reihe von TeXLive-Paketen installiert sein (siehe Dokumentation). Als Alternativen seien an dieser Stelle das in Calibre integrierte Konvertierungstool ebook-convert und epub2pdf genannt.

#Papierformat (Standard ist US-Letter), leere Seiten vermeiden
latex_elements = {
    'papersize': 'a4paper',
    'classoptions': 'oneside,openany'
}

#Logo auf der Titelseite
latex_logo = '_static/logo.png'

Sonstiges

Pygments
Syntax-Highlighting, ebenfalls von Nikola unterstützt, hier wie dort bevorzuge ich das Theme "borland".
Bezeichnung der Ausgabedatei
Der Dateiname lässt sich für die verschiedenen Builder jeweils festlegen. Um für alle verwendeten Builder jeweils die gleiche Bezeichnung zu nutzen, verwende ich hier die eigene Variable output_basename. Diese wird demzufolge nicht von Sphinx unterstützt und nur innerhalb der conf.py verwendet (in den Variablen htmlhelp_basename, latex_documents, texinfo_documents, epub_basename).
pygments_style = 'borland'
output_basename = 'gladepytutorial'

_static

In diesem Ordner befinden sich Stylesheets, Bilder und Skripte, die nach den vorgegebenen Dateien geladen werden. So kann man lokale individuelle Stylesheet-Anpassungen vornehmen, ohne das Theme selbst zu modifizieren. Hier befinden sich eine angepasste pygments.css, cover.png (Epub) und logo.png (PDF).

_templates

Analog zu _static befinden sich hier individuelle Templates. Diese werden standardmäßig mit der Template-Engine Jinja2 betrieben. Hier befindet sich info.html, die in der Epub-Ausgabe Verwendung findet.

Epub: zusätzliche Dateien einfügen

Sphinx bietet mit epub_pre_files (und analog epub_post_files) die Option, zusätzliche und nicht zur eigentlichen Dokumentation gehörenden (X)HTML-Dateien zum Epub hinzuzufügen. Diese müssen allerdings zunächst als zusätzliche HTML-Seiten generiert werden. Dafür wird in der conf.py die Variable html_additional_pages entsprechend gesetzt [1].

[1] Es hat mich einen (EINEN!) Tag gekostet dies herauszufinden...

index.rst

Dies ist das Hauptdokument, das von jedem Sphinx-Builder geparst wird. Die Bezeichnung wird in der conf.py in der Variable master_doc festgelegt.

Als reguläre reST-Datei kann sie beliebig viel Inhalt aufnehmen. Es ist allerdings zu empfehlen und im Normalfall vermutlich sowieso bereits der Fall, das Dokument in mehrere Dateien aufzuteilen. Sphinx stellt dafür die eigene toctree-Directive zur Verfügung.

.. toctree::
    :maxdepth: 1
    :numbered:
    :caption: Inhalt

    teildokument1
    teildokument2
    ...

Dateien außerhalb von toctree werden per include-Directive hinzugefügt.

Es ist auch möglich, Inhalte nur von bestimmten Buildern berücksichtigen zu lassen:

.. only:: latex

    .. include:: info.rst

Bonus: Mobi

"Ich habe doch einen Kindle und hätte auch gern so ein E-Book!"

Aber klar doch.

KindleGen

Amazon möchte zwar keine Epubs [2] unterstützen, aber sie bieten mit KindleGen ein Tool an, welches diese in die eigenen Formate (KF8, Mobi) überführt.

Auf diese Weise lässt sich mit

$ kindlegen input.epub

eine Mobi-Datei erzeugen.

[2] oder Google-Apps...

Problem: Encoding

Das aus dem Epub erstellte E-Book im Mobi-Format hat ein Darstellungsproblem mit einigen (Sonder-)Zeichen.

Abhilfe schafft hier die Zeile

<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />

an Stelle von

<meta charset="utf-8" />

innerhalb des HTML-Heads. Sphinx bietet dafür die meta-Directive, die allerdings für jede Datei gesetzt werden muss:

.. meta::
    :http-equiv=Content-Type: text/html; charset=UTF-8

Nikola

Das Resultat des ersten Durchlaufs von Sphinx mit der Übersichtsseite und drei Artikeln lässt vorsichtig optimistisch werden. Trotzdem gibt es an diversen Stellen Optimierungsbedarf:

  1. Die Nikola-eigenen Kurzverweise (slug) funktionieren nicht und erfordern eine Konvertierung in ":ref:"erenz.

  2. Die Artikelüberschrift ist kein Gliederungselement und fehlt demzufolge im Inhaltsverzeichnis

  3. Nikola-eigene Directives verursachen Fehler. Konvertierung von

    • thumbnail -> image
    • listings -> literalinclude
  4. relative Pfade in image-Directives anpassen

  5. Animierte GIFs ignorieren (erzeugen Fehler im LaTeX-Durchlauf, aber nicht im Epub)

  6. Inhaltsverzeichnisse in den Artikel überflüssig

  7. "Kommentieren auf G+"-Button entfernen

  8. Für die Generierung der Mobi-Datei muss jede Datei eine Meta-Anweisung erhalten

Für eine zufriedenstellende Ausgabe ist es also erforderlich, die Ausgangsdateien hinsichtlich dieser Punkte per Skript zu modifizieren.

Automatisierung

Das ist er, der Elefant im Raum.

Sphinx läuft und die index.rst ist eingerichtet. Die Mission besteht nun aus folgenden Teilaufgaben:

  1. Sphinx soll sich der aktuellen Dateien der GitHub-Page bedienen.
  2. Diese Dateien sollen gemäß der oben genannten Punkte bearbeitet werden.
  3. Sphinx soll ein Epub und ein PDF erzeugen.
  4. KindleGen soll ein Mobi erzeugen.
  5. Die Dateien sollen im entsprechenden Ordner im GitHub Page-Verzeichnis abgelegt und deployt werden.

Let's do this.

Die diffizile Arbeit ist bereits erledigt: die Einrichtung von Sphinx und die Problemerfassung. Das Skript selbst arbeitet nun die oben genannten Punkte ab. Weiterhin gibt es der Übersichtlichkeit halber zwei weitere Dateien. Es befinden sich nun im Sphinx-Projektverzeichnis folgende neue Dateien:

  1. nibook.py: sammelt, kopiert, bearbeitet die Quelldateien, erstellt die E-Books und füttert die GitHub-Page (Code)
  1. index.lst: Liste von Dateinamen (ohne Endung), die im Dokument enthalten sein sollen
übersicht
artikel1
artikel2
artikel5
  1. index.tmpl: aus dieser und der index.lst wird die index.rst generiert
.. generated by nibook, posts will be inserted after ".. include-start"

.. some text documentation master file, created by
   sphinx-quickstart on Thu Oct 26 20:26:54 2017.
   You can adapt this file completely to your liking, but it should at least
   contain the root `toctree` directive.

.. meta::
   :http-equiv=Content-Type: text/html; charset=UTF-8

.. only:: latex

    .. include:: info.rst

****************************
Glade-Tutorial mit PyGObject
****************************

.. toctree::
    :maxdepth: 1
    :numbered:
    :caption: Inhalt
    :name: mastertoc

    .. include-start

Fazit

Wieder was gelernt.


Kommentieren auf

New widgets in old Glade files

Problem: disabled widgets in older Glade files

It may occur that certain widgets in Glade are greyed out after opened in Glade.

/images/glade_widgetsinactive.thumbnail.png

A hint is hown in tooltips like

/images/glade_widgetsinactive_tooltip.thumbnail.png

The cause for this behaviour is the Gtk+ version given at the beginning of the file:

<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.20.1 -->
<interface>
  <requires lib="gtk+" version="3.0"/>
  ...

This line will not be altered even if the file is saved by a more recent Glade version. So you just manually replace "3.0" by a current version ("3.20" at present).

BeeLine - keep on riding

Further testing of the navigation compass BeeLine

See also my introductory BeeLine article.

It's alive!

BeeLine lives. There has been made some nice development progress. You could almost assume somebody from the team read my first article because nearly everything mentioned in the improvement section has been realized.

Another positive aspect is communication. The team reacts very fast on interactions on Twitter and via e-mail. It's quite exhilaranting to experience that there are real people working behind a product/brand...(yes, I'm looking at you, Google).

Improvements

My four points of criticism were:

  1. lack of practicality on waypoint handling
  2. accidentally finish rides
  3. unclear button assignments
  4. wish for better route functionalities (import)

Waypoints

Manual scrolling

Scrolling through route waypoints is accomplished by pressing the N/S buttons. If you reach the last waypoint you can end your ride by confirming the "Arrived?" question.

There still is no information on waypoint progress on the device but it is shown in the app.

/images/BeeLine/app_waypoint.png

App: waypoint status

Automatic routing

If "Automatic Waypoint" is activated in the settings the next waypoint will be selected when passing the current one less than 100 meters. If you keep a distance larger than that Beeline will not change the selected waypoint even if another waypoint is closer.

When choosing automatic waypoint routing there always remains the option to manually skip waypoints.

I regard this to be a real killer feature which makes routing simple and precise while remaining discreet and in no way annoying.

Button assignment

As already said skipping through waypoints is done by pressing the top or bottom button. Pressing and holding the bottom button switches the integrated lamp on and off. The right and left buttons let you flip through

Compass <-> Speed <-> Time <-> Battery status (phone and BeeLine) <-> Compass

It did not happen anymore that I accidentally finished a ride, so this problem vanished.

Route import (whoop, whoop)

Not only can a route be created via BeeLine app but also be imported from a GPX file. In doing so the track will be simplified and (significant) changes of direction will be converted into waypoints. Two waypoints are always connected by a straight line (beeline).

/images/BeeLine/route_import.png

Left: imported simplified route with waypoints. Right: original GPS track in Locus app

As you can see this works pretty nice. This route conversion provides waypoints for relevant route details (important changes of direction, crossroads, bridges) without determining a certain track layout.

/images/BeeLine/waypoint_route.png

Only few waypoints necessary to outline route

Make a wish

To avoid this article to be suspected of being some sponsored shit I reserved some space for my personal feature requests:

Ride routes in opposite direction

This would save some work for creating routes in reverse order.

Waypoint progress

I'd like to have that waypoint progress status shown on the device like it is displayed in the app ("Waypoint 6/20"). Yes, I know that this is unrelated to distances. That information does not have to be permanently present, I can imagine it to be integrated as a page on the right/left button page scroll function.

Pause ride

A funtion to pause/continue routes instead of finishing them would be helpful (for example to proceed on long tours or when spontaneously jumping into a nearby supermarket).

Calibration

We all hate it: the horseshoe symbol on the Beeline - bar left, bar right, front wheel up and agein. This is easy with the racing bike but a torture with the pannier packed trucker.

I don't really have a constructive suggestion here. The calibration horseshoe often appears during a ride and disappears after a while (sometimes within seconds). I mostly ignore it now.

Me vs. me

  1. I often ride the same routes.
  2. Everybody likes statistics.

So why not show a route summary with some information (frequency, duration, distance, speed)?

Conclusion

BeeLine has developed great and I will use it more frequently now. By expanding route functionalities (create, import, save, automatic routing) the device has become a really interesting and useful tool rather than just being a gadget.

Of course BeeLine still depends on a smartphone connection providing location service and Bluetooth. It cannot replace a standalone navigation system. Therefore battery consumption of the phone has to be considered on long tours. Power banks exist.


Comment on

Stacks and notebooks

Organize contents on screen

Gtk.Stack and Gtk.Notebook are layout containers that can hold any widgets.

A notebook provides a multipage layout including a classic tab functionality. A stack also provides this basic functionality and you will able to switch between different layout pages.

The main difference is the control widget of the stack is a separate widget (Gtk.StackSwitcher). Several stack switcher widgets can be assigned to one stack. A stack switcher can be placed into the headerbar and animated transitions between stack pages are supported.

Stack subjectively fit better into the GNOME environment but notebooks provide more customization/functionality options.

In the example there is a window containing a stack including a notebook on the third page showing different websites.

/images/21_stacknotebook.thumbnail.png

Glade

Stack

Stacks can be found in the sidebar's 'Container' section. Pages are easily created and edited via Glade. Sub-widgets in the example file are Gtk.Image, Vte.Terminal and Gtk.Notebook.

The stack switcher widget can be obtained from 'Controls and Display' and is placed to the headerbar. It's also possible to put it into a regular container widget like boxes. Pages can be shown in vertical or horizontal order. In "General > Stack" a stack element must be assigned to the widget. The page name shown by the stack switcher widget can be edited via "Packing > Title" of the sub-widget. This sub-widget has to be created in the first place, a new created stack has empty pages.

Notebook

Notebook can also be found in the 'Container' section. The tab's control unit is an integrated label child widget automatically generated on page creation. Gtk.ScrolledWindows are used here as the pages' container widgets. These are also required for displaying (long) tables (see also List-/TreeStore articles No. 1 und No. 2).

The tab bar of a notebook provides reserved space for additional widgets like fixed buttons ("General > Start/End Action"). In the example there will be created a "Home" button in the start area.

Python

There are no signals required for switching between stack pages and notebook tabs. In the example only two signals are assigned, one for catching the "exit" command within the terminal and one for the button in the notebook tab bar.

WebKit2

The webpages in the example are rendered by WebKit2. The essential module to use is WebKit2.WebView. A new WebView object itself already is a scrollable Gtk+ widget within a Gtk.Viewport element. According to the API reference it does not have to be placed in a Gtk.ScrolledWindow container widget. Having tested this that works for Gtk.Stack but not for Gtk.Notebook. That's why in the example there is used a ScrolledWindow as underlying container widget.

The following pattern is used to create a WebView widget:

#create new WebView widget
webview = WebKit2.WebView()
#send URL to widget
webview.load_uri("http://google.com")
#add webview to notebook
notebook.add(webview)
#add webview to stack
stack.add_titled(webview, name, "StackSwitcher title")

webview.show()

Read more…