Texteditor mit GtkSourceView
Inhalt
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.

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
undactivate
)"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())

Listings
Python
#!/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.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>
Kommentare bei
Das Geheimnis der Sphinx
Inhalt
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.

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 derconf.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:
Die Nikola-eigenen Kurzverweise (slug) funktionieren nicht und erfordern eine Konvertierung in "
:ref:
"erenz.Die Artikelüberschrift ist kein Gliederungselement und fehlt demzufolge im Inhaltsverzeichnis
-
Nikola-eigene Directives verursachen Fehler. Konvertierung von
thumbnail -> figure
listings -> literalinclude
relative Pfade in image-Directives anpassen
Animierte GIFs ignorieren (erzeugen Fehler im LaTeX-Durchlauf, aber nicht im Epub)
Inhaltsverzeichnisse in den Artikel überflüssig
"Kommentieren auf G+"-Button entfernen
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:
Sphinx soll sich der aktuellen Dateien der GitHub-Page bedienen.
Diese Dateien sollen gemäß der oben genannten Punkte bearbeitet werden.
Sphinx soll ein Epub und ein PDF erzeugen.
KindleGen soll ein Mobi erzeugen.
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:
nibook.py: sammelt, kopiert, bearbeitet die Quelldateien, erstellt die E-Books und füttert die GitHub-Page (Code)
index.lst: Liste von Dateinamen (ohne Endung), die im Dokument enthalten sein sollen
übersicht artikel1 artikel2 artikel5
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
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.

A hint is hown in tooltips like

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).
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.

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()
Listings
Python
#!/usr/bin/python # -*- coding: utf-8 -*- import sys import urllib.request import gi gi.require_version("Gtk", "3.0") gi.require_version("Vte", "2.91") gi.require_version("WebKit2", "4.0") from gi.repository import Gtk, Gio, Vte, GObject, GLib, WebKit2 class Handler: def on_term_child_exited(self, widget, event): # reset and setup terminal on exit command widget.reset(True, True) app.stack_console() def on_home_button_clicked(self, widget): # reload given URL in current tab page = app.obj("notebook").get_current_page() app.nbtabs[page][2].load_uri(app.nbtabs[page][1]) class ExampleApp: 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): GObject.type_register(Vte.Terminal) builder = Gtk.Builder() builder.add_from_file("21_stacknotebook.glade") builder.connect_signals(Handler()) self.obj = builder.get_object self.obj("window").set_application(app) self.obj("window").show_all() # get window content self.stack_image() self.stack_console() self.stack_notebook() def run(self, argv): self.app.run(argv) def stack_image(self): # download and show NASA Astonomy Picture of the Day URL = "https://apod.nasa.gov" source = urllib.request.urlopen(URL).read().decode("utf-8") img_start = source.find("<IMG SRC=") img_end = source.find("alt=") img = source[img_start+10:img_end-2] IMGURL = "https://apod.nasa.gov/apod/" + img urllib.request.urlretrieve(IMGURL, "apod.jpg") self.obj("image").set_from_file("apod.jpg") def stack_console(self): # setup terminal self.obj("term").spawn_sync( Vte.PtyFlags.DEFAULT, None, ["/bin/bash"], None, GLib.SpawnFlags.DEFAULT, ) def stack_notebook(self): self.nbtabs = [ ["gi_doc", "https://lazka.github.io/pgi-docs/"], ["gtk_tut", "http://python-gtk-3-tutorial.readthedocs.io/en/latest/index.html"], ["glade_tut", "https://encarsia.github.io/posts/tutorial-reihe-glade/"] ] for tab in self.nbtabs: webview = WebKit2.WebView() tab.append(webview) webview.load_uri(tab[1]) self.obj(tab[0]).add(webview) webview.show() app = ExampleApp() app.run(sys.argv)
Glade
21_stacknotebook.glade (Source)
<?xml version="1.0" encoding="UTF-8"?> <!-- Generated with glade 3.20.1 --> <interface> <requires lib="gtk+" version="3.20"/> <requires lib="vte-2.91" version="0.50"/> <object class="GtkImage" id="image1"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="icon_name">go-home</property> </object> <object class="GtkApplicationWindow" id="window"> <property name="width_request">800</property> <property name="height_request">600</property> <property name="can_focus">False</property> <child> <object class="GtkStack" id="stack"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="transition_type">crossfade</property> <child> <object class="GtkImage" id="image"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="stock">gtk-missing-image</property> </object> <packing> <property name="name">page0</property> <property name="title" translatable="yes">Astronomy Picture of the Day</property> </packing> </child> <child> <object class="VteTerminal" id="term"> <property name="visible">True</property> <property name="can_focus">True</property> <property name="hscroll_policy">natural</property> <property name="vscroll_policy">natural</property> <property name="encoding">UTF-8</property> <property name="scroll_on_keystroke">True</property> <property name="scroll_on_output">False</property> <signal name="child-exited" handler="on_term_child_exited" swapped="no"/> </object> <packing> <property name="name">page3</property> <property name="title" translatable="yes">Terminal</property> <property name="position">1</property> </packing> </child> <child> <object class="GtkNotebook" id="notebook"> <property name="visible">True</property> <property name="can_focus">True</property> <child> <object class="GtkScrolledWindow" id="gi_doc"> <property name="visible">True</property> <property name="can_focus">True</property> <property name="shadow_type">in</property> <child> <placeholder/> </child> </object> </child> <child type="tab"> <object class="GtkLabel"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="label" translatable="yes">PyGObject API Reference </property> </object> <packing> <property name="tab_fill">False</property> </packing> </child> <child> <object class="GtkScrolledWindow" id="gtk_tut"> <property name="visible">True</property> <property name="can_focus">True</property> <property name="shadow_type">in</property> <child> <placeholder/> </child> </object> <packing> <property name="position">1</property> </packing> </child> <child type="tab"> <object class="GtkLabel"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="label" translatable="yes">Python GTK+ 3 Tutorial</property> </object> <packing> <property name="position">1</property> <property name="tab_fill">False</property> </packing> </child> <child> <object class="GtkScrolledWindow" id="glade_tut"> <property name="visible">True</property> <property name="can_focus">True</property> <property name="shadow_type">in</property> <child> <placeholder/> </child> </object> <packing> <property name="position">2</property> </packing> </child> <child type="tab"> <object class="GtkLabel"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="label" translatable="yes">Glade-Tutorial</property> </object> <packing> <property name="position">2</property> <property name="tab_fill">False</property> </packing> </child> <child type="action-start"> <object class="GtkButton" id="home_button"> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">True</property> <property name="image">image1</property> <property name="always_show_image">True</property> <signal name="clicked" handler="on_home_button_clicked" swapped="no"/> </object> <packing> <property name="tab_fill">False</property> </packing> </child> </object> <packing> <property name="name">page1</property> <property name="title" translatable="yes">Notebook</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="GtkStackSwitcher"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="stack">stack</property> </object> </child> </object> </child> </object> </interface>
Comment on
NoN: Konsoledierung
Knights of Ni - jetzt wird's schick
Einmal eingerichtet, benötigt man für das Befeuern einer Nikola-angetriebenen Seite nur einen Dateimanager, einen Editor, ein Terminal und normalerweise zwei Kommandos.
Und weil ich es gern bequem habe, habe ich mir etwas Unterstützung dafür gebastelt.
Was bisher geschah
Die per Button aufgerufenen Nikola-Kommandos wurden bisher folgendermaßen verarbeitet:
nikola build
lief im Hintergrund als subprocess.run(cmd)
nikola github_deploy
wurde im separaten Terminalfenster ausgeführt; dieses wurde nach der erfolgreichen Ausführung wieder geschlossen
Und das soll jetzt alles vorbei sein?
Neu ist immer besser.
—Barney Stinson
Die Oberfläche ist nun per Gtk.Stack zweigeteilt. Per Gtk.StackSwitcher in der Headerbar lässt sich zwischen der normalen Oberfläche und einem Terminal hin- und herwechseln.
Dies hat mehrere Eigenschaften und Vorteile:
Das Teminal öffnet sich im aktuellen Verzeichnis der Nikola-Instanz.
Das Terminal kann beliebig verwendet werden.
Beim
exit
wird es nur resettet.
build
undgithub_deploy
werden in diesem Terminal ausgeführt, wenn sie über die Oberfläche (Buttons) gestartet werden.Beim Ausführen über die Buttons wechselt der Focus auf das Terminal und nach Beenden des Tasks wieder zurück zur Oberfläche. Mit super Überblende (aktivierte Animationen erforderlich)!
Optisch aufgeräumter, da kein separates Fenster mehr benötigt wird.

Media player with VLC
Contents
Creating a media player with LibVLC
VLC is not just a multimedia player but also a framework with Python bindings available. In this example app a simple media player will be set up via LibVLC (see also the GStreamer mediaplayer article).

LibVLC
The installation of the VLC Python bindings are mandatory. The package is coomonly found under the name python-vlc
.
Glade
display area of the media file: Gtk.DrawingArea widget
control elements: skip for-/backward (Gtk.Button), pause/resume playback (Gtk.Togglebutton)
select media: buttons to show video or image file
manipulate playback: buttons to mute and rotate video
Python
Set up player
The VLC player is initiated when the corresponding widget (Gtk.DrawingArea) is drawn. The realize
is required for that task. This signal in general is available for the widget class.
vlcOptions = "--no-xlib" win_id = widget.get_window().get_xid() setup_player(vlcOptions) vlcInstance = vlc.Instance(options) player = vlcInstance.media_player_new() player.set_xwindow(win_id)
Given options can be regular VLC commandline options. In the example app a click on the "rotate" button turns the video 180 degrees. Therefore the player must be initiated again with the option --video-filter=transform{type=180}
given.
Media playback
Just like the GStreamer player VLC is capable of showing various video, audio and image formats.
player.set_mrl(file_url) #start playback player.play() #pause/resume playback player.pause()
Position scale
The implementation of the progress bar using a slide control is pretty simple.
#retrieve position player.get_position() #define positition player.set_position(val)
Possible values are float numbers between 0 and 1. These functions are quite resource demanding resulting into stuttering playback. In this example the get_position
is avoided by retrieving the slider position instead of the video.
Possibilities and limitations
Working with LibVLC Python bindings is easy and intuitive in contrast to GStreamer. In addition the "headerbar problem" is non-existent.
On the other hand it is not quite minimalistic to resort to a huge and indepentant project. You will have to install VLC and Python bindings instead of just importing the GStreamer module from the GObject Introspection repository.
The overall consumption of resources is bigger.
Listings
Glade
<?xml version="1.0" encoding="UTF-8"?> <!-- Generated with glade 3.20.0 --> <interface> <requires lib="gtk+" version="3.16"/> <object class="GtkAdjustment" id="adjustment"> <property name="upper">100</property> <property name="step_increment">1</property> <property name="page_increment">10</property> </object> <object class="GtkImage" id="image1"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="stock">gtk-media-rewind</property> </object> <object class="GtkImage" id="image2"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="stock">gtk-media-forward</property> </object> <object class="GtkImage" id="image3"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="stock">gtk-media-pause</property> </object> <object class="GtkApplicationWindow" id="window"> <property name="width_request">600</property> <property name="height_request">500</property> <property name="can_focus">False</property> <property name="default_width">440</property> <property name="default_height">250</property> <child> <object class="GtkBox" id="box1"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="orientation">vertical</property> <child> <object class="GtkDrawingArea" id="play_here"> <property name="visible">True</property> <property name="can_focus">False</property> <signal name="realize" handler="on_play_here_realize" swapped="no"/> </object> <packing> <property name="expand">True</property> <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> <object class="GtkSeparator" id="separator1"> <property name="visible">True</property> <property name="can_focus">False</property> </object> <packing> <property name="expand">False</property> <property name="fill">True</property> <property name="position">1</property> </packing> </child> <child> <object class="GtkBox" id="box3"> <property name="visible">True</property> <property name="can_focus">False</property> <child> <object class="GtkButtonBox" id="buttonbox1"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="layout_style">start</property> <child> <object class="GtkButton" id="backward"> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">True</property> <property name="image">image1</property> <property name="always_show_image">True</property> <signal name="clicked" handler="on_backward_clicked" swapped="no"/> </object> <packing> <property name="expand">True</property> <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> <object class="GtkButton" id="forward"> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">True</property> <property name="image">image2</property> <property name="always_show_image">True</property> <signal name="clicked" handler="on_forward_clicked" swapped="no"/> </object> <packing> <property name="expand">True</property> <property name="fill">True</property> <property name="position">1</property> </packing> </child> <child> <object class="GtkToggleButton" id="playpause_togglebutton"> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">True</property> <property name="image">image3</property> <property name="always_show_image">True</property> <signal name="toggled" handler="on_playpause_togglebutton_toggled" swapped="no"/> </object> <packing> <property name="expand">True</property> <property name="fill">True</property> <property name="position">2</property> </packing> </child> </object> <packing> <property name="expand">False</property> <property name="fill">False</property> <property name="position">0</property> </packing> </child> <child> <object class="GtkScale" id="progress"> <property name="width_request">300</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="halign">center</property> <property name="margin_left">5</property> <property name="margin_right">5</property> <property name="adjustment">adjustment</property> <property name="fill_level">100</property> <property name="round_digits">1</property> <property name="draw_value">False</property> <signal name="change-value" handler="on_progress_change_value" swapped="no"/> </object> <packing> <property name="expand">False</property> <property name="fill">False</property> <property name="position">1</property> </packing> </child> </object> <packing> <property name="expand">False</property> <property name="fill">True</property> <property name="position">2</property> </packing> </child> <child> <object class="GtkButtonBox" id="buttonbox2"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="homogeneous">True</property> <property name="layout_style">expand</property> <child> <object class="GtkButton" id="vbutton"> <property name="label" translatable="yes">Video</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">True</property> <signal name="clicked" handler="on_vbutton_clicked" swapped="no"/> </object> <packing> <property name="expand">True</property> <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> <object class="GtkButton" id="ibutton"> <property name="label" translatable="yes">Image</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">True</property> <signal name="clicked" handler="on_ibutton_clicked" swapped="no"/> </object> <packing> <property name="expand">True</property> <property name="fill">True</property> <property name="position">1</property> </packing> </child> <child> <object class="GtkToggleButton" id="mute"> <property name="label" translatable="yes">Mute</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">True</property> <signal name="toggled" handler="on_mute_toggled" swapped="no"/> </object> <packing> <property name="expand">False</property> <property name="fill">True</property> <property name="position">2</property> </packing> </child> <child> <object class="GtkToggleButton" id="rotate"> <property name="label" translatable="yes">Rotate</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">True</property> <signal name="toggled" handler="on_rotate_toggled" swapped="no"/> </object> <packing> <property name="expand">True</property> <property name="fill">True</property> <property name="position">3</property> </packing> </child> </object> <packing> <property name="expand">False</property> <property name="fill">True</property> <property name="position">4</property> </packing> </child> </object> </child> <child type="titlebar"> <object class="GtkHeaderBar"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="title">VLC based media player</property> <property name="show_close_button">True</property> <child> <placeholder/> </child> </object> </child> </object> </interface>
Python
20_vlc_simpleplayer.py (Source)
#!/usr/bin/env python # -*- coding: utf-8 -*- import os import subprocess import sys import time import vlc import gi gi.require_version("Gtk", "3.0") from gi.repository import Gtk, Gio, Gdk, GLib class Handler: def on_play_here_realize(self, widget): vlcOptions = "--no-xlib" self.win_id = widget.get_window().get_xid() self.setup_player(vlcOptions) self.player.audio_set_mute(False) self.is_playing = False def on_rotate_toggled(self, widget): pos = self.player.get_position() self.player.stop() self.player.release() self.vlcInstance.release() if widget.get_active(): vlcOptions = "--no-xlib --video-filter=transform{type=180}" else: vlcOptions = "--no-xlib" self.setup_player(vlcOptions) self.player.set_mrl(self.video) self.player.play() self.player.set_position(pos) if not self.is_playing: time.sleep(.05) self.player.pause() def setup_player(self, options): self.vlcInstance = vlc.Instance(options) self.player = self.vlcInstance.media_player_new() self.player.set_xwindow(self.win_id) def on_backward_clicked(self, widget): skip_pos = go.slider.get_value() - 10 if skip_pos < 0: self.player.set_position(0) go.slider.set_value(0) else: self.player.set_position(skip_pos / 100) go.slider.set_value(skip_pos) def on_forward_clicked(self, widget): skip_pos = go.slider.get_value() + 10 if skip_pos > 100: self.player.pause() self.player.set_position(0.99) go.slider.set_value(100) else: self.player.set_position(skip_pos / 100) go.slider.set_value(skip_pos) def on_playpause_togglebutton_toggled(self, widget): if widget.get_active(): img = Gtk.Image.new_from_icon_name(Gtk.STOCK_MEDIA_PLAY, Gtk.IconSize.BUTTON) widget.set_property("image", img) self.is_playing = False else: img = Gtk.Image.new_from_icon_name(Gtk.STOCK_MEDIA_PAUSE, Gtk.IconSize.BUTTON) widget.set_property("image", img) self.is_playing = True self.player.pause() GLib.timeout_add(1000, self.update_slider) def on_vbutton_clicked(self, widget): self.video = "file://" + os.path.abspath("mediaplayer.avi") self.duration = go.get_duration(self.video) self.player.set_mrl(self.video) self.is_playing = True go.slider.set_value(0) go.obj("playpause_togglebutton").set_active(False) go.obj("playpause_togglebutton").set_sensitive(True) go.obj("mute").set_sensitive(True) go.obj("rotate").set_sensitive(True) self.player.play() GLib.timeout_add(1000, self.update_slider) def on_ibutton_clicked(self, widget): image = "file://" + os.path.abspath("mediaplayer.jpg") self.player.set_mrl(image) self.is_playing = False self.player.play() go.obj("playpause_togglebutton").set_sensitive(False) go.obj("mute").set_sensitive(False) go.obj("rotate").set_sensitive(False) def on_mute_toggled(self, widget): if widget.get_active(): widget.set_label("Unmute") else: widget.set_label("Mute") self.player.audio_toggle_mute() def on_progress_change_value(self, widget, scroll, value): self.player.set_position(value / 100) widget.set_value(value) def update_slider(self): if not self.is_playing: return False # cancel timeout else: pos = go.slider.get_value() new_pos = (pos + 100 / self.duration) go.slider.set_value(new_pos) if new_pos > 100: self.is_playing = False return True # continue calling every x milliseconds class VlcPlayer: def __init__(self): self.app = Gtk.Application.new("org.media.player", Gio.ApplicationFlags(0)) self.app.connect("activate", self.on_app_activate) def on_app_activate(self, app): # setting up builder builder = Gtk.Builder() builder.add_from_file("20_vlc_player.glade") builder.connect_signals(Handler()) self.obj = builder.get_object # slider position is float between 0..100 self.slider = self.obj("progress") window = self.obj("window") window.set_application(app) window.show_all() def get_duration(self,video): command = ["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", video, ] ffprobe_cmd = subprocess.run(command, stdout=subprocess.PIPE) # stdout of subprocess is byte variable, convert into float then into integer return int(float(ffprobe_cmd.stdout.decode())) def run(self, argv): self.app.run(argv) go = VlcPlayer() go.run(None)
Media player with GStreamer
Contents
Creating a media player with GStreamer 1.x
GStreamer is a multimedia framework that can be used ti show (de)code and otherwise alter media files.

Glade
display area of the media file: Gtk.DrawingArea widget
control elements: skip for-/backward (Gtk.Button), pause/resume playback (Gtk.Togglebutton)
select media: buttons to show video or image file
Python
Set up player
Elements and pipelines
GStreamer manages all kinds of media data streams. Every step in the procession chain is defined as an element connected to pipelines. A common pipeline consists of "source", "filter"/"decode" and "sink" elements.
------------------------------------------------------ | pipeline | | | | ------------- ---------------- -------------- | | | source | | filter | | sink | | | | |->>| decoder |->>| | | | | input | | processing | | output | | | ------------- ---------------- -------------- | ------------------------------------------------------
This is done by the Gst module:
#init Gst and create pipeline Gst.init() pipeline = Gst.Pipeline() #create elements src = Gst.ElementFactory.make("filesrc","source") decode = Gst.ElementFactory.make("decodebin","decode") sink = Gst.ElementFactory.make("xvimagesink") #configure elements src.set_property("location",file_location) #add elements to pipeline pipeline.add(src) pipeline.add(decode) pipeline.add(sink) #link elements together src.link(decode) decode.link(sink)
Predefined pipelines
There are plenty of possibilities such like handling audio and video signals separated from each other by assigning a "videosink" and an "audiosink" and so on. On the other hand there are given pipelines for standard tasks like media playback. In this case there can be made use of the "playbin" element which also significantly reduces the code:
Gst.init(None) player = Gst.ElementFactory.make("playbin","player") sink = Gst.ElementFactory.make("xvimagesink") player.set_property("uri",uri_of_file) player.set_property("video-sink", sink)
And action!
A pipeline or playbin element can now be controled by Gst.STATE: .. code-block:: python
player.set_state(Gst.State.PLAYING) player.set_state(Gst.State.PAUSED)
Progress bar
The video progress in this example will not be visualized by a Gtk.ProgressBar but by a horizontal Gtk.Scale. This widget allows to manually set a position with the mouse instead of just showing a value using the value-changed signal. Strictly speaking the change-value signal is a much cleaner solution here which will be used in the follow-up article on relizing the media player with LibVLC.
Possibilities and limitations
Getting to know how to utilize GStreamer there appear a bunch of obstacles (read as: the incompetent author of this article tend to widely generalize based on her experiences):
There are plenty of tutorials but two circumstances make them difficult to comply with:
The primary language in GStreamer is C. Good luck with your Python stuff.
Many older tutorials and manuals do not work out of the box because of major version leap of both GStreamer (0.10 to 1.x) and Python (2.x auf 3.x).
In addition there are effects that are hard to understand. The example given in this article does not work if the Gtk window contains a headerbar. In theory this should be solved by using the "gtksink" but I haven't figured out yet how to assign that sink to a specific widget.
Listings
Glade
<?xml version="1.0" encoding="UTF-8"?> <!-- Generated with glade 3.20.0 --> <interface> <requires lib="gtk+" version="3.16"/> <object class="GtkAdjustment" id="adjustment"> <property name="upper">100</property> <property name="step_increment">1</property> <property name="page_increment">10</property> </object> <object class="GtkImage" id="image1"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="stock">gtk-media-rewind</property> </object> <object class="GtkImage" id="image2"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="stock">gtk-media-forward</property> </object> <object class="GtkImage" id="image3"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="stock">gtk-media-pause</property> </object> <object class="GtkWindow" id="window"> <property name="can_focus">False</property> <property name="title" translatable="yes">GStreamer media player</property> <property name="default_width">600</property> <property name="default_height">350</property> <signal name="destroy" handler="on_window_destroy" swapped="no"/> <child> <object class="GtkBox" id="box1"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="orientation">vertical</property> <child> <object class="GtkDrawingArea" id="play_here"> <property name="visible">True</property> <property name="can_focus">False</property> </object> <packing> <property name="expand">True</property> <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> <object class="GtkSeparator" id="separator1"> <property name="visible">True</property> <property name="can_focus">False</property> </object> <packing> <property name="expand">False</property> <property name="fill">True</property> <property name="position">1</property> </packing> </child> <child> <object class="GtkBox" id="box3"> <property name="visible">True</property> <property name="can_focus">False</property> <child> <object class="GtkButtonBox" id="buttonbox1"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="layout_style">start</property> <child> <object class="GtkButton" id="backward"> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">True</property> <property name="image">image1</property> <property name="always_show_image">True</property> <signal name="clicked" handler="on_backward_clicked" swapped="no"/> </object> <packing> <property name="expand">True</property> <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> <object class="GtkButton" id="forward"> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">True</property> <property name="image">image2</property> <property name="always_show_image">True</property> <signal name="clicked" handler="on_forward_clicked" swapped="no"/> </object> <packing> <property name="expand">True</property> <property name="fill">True</property> <property name="position">1</property> </packing> </child> <child> <object class="GtkToggleButton" id="playpause_togglebutton"> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">True</property> <property name="image">image3</property> <signal name="toggled" handler="on_playpause_togglebutton_toggled" swapped="no"/> </object> <packing> <property name="expand">True</property> <property name="fill">True</property> <property name="position">2</property> </packing> </child> </object> <packing> <property name="expand">False</property> <property name="fill">False</property> <property name="position">0</property> </packing> </child> <child> <object class="GtkScale" id="progress"> <property name="width_request">300</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="halign">center</property> <property name="margin_left">5</property> <property name="margin_right">5</property> <property name="adjustment">adjustment</property> <property name="fill_level">100</property> <property name="round_digits">1</property> <property name="draw_value">False</property> </object> <packing> <property name="expand">False</property> <property name="fill">False</property> <property name="position">1</property> </packing> </child> </object> <packing> <property name="expand">False</property> <property name="fill">True</property> <property name="position">2</property> </packing> </child> <child> <object class="GtkBox" id="box2"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="homogeneous">True</property> <child> <object class="GtkButton" id="vbutton"> <property name="label" translatable="yes">Play video</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">True</property> <signal name="clicked" handler="on_vbutton_clicked" swapped="no"/> </object> <packing> <property name="expand">True</property> <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> <object class="GtkButton" id="ibutton"> <property name="label" translatable="yes">Show image</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">True</property> <signal name="clicked" handler="on_ibutton_clicked" swapped="no"/> </object> <packing> <property name="expand">True</property> <property name="fill">True</property> <property name="position">1</property> </packing> </child> </object> <packing> <property name="expand">False</property> <property name="fill">True</property> <property name="position">3</property> </packing> </child> </object> </child> <child> <placeholder/> </child> </object> </interface>
Python
19_gst_simpleplayer.py (Source)
#!/usr/bin/env python # -*- coding: utf-8 -*- import os import time import gi gi.require_version("Gtk", "3.0") gi.require_version("Gst", "1.0") gi.require_version("GstVideo", "1.0") from gi.repository import Gst, Gtk, GLib, GstVideo class GenericException(Exception): pass class Handler: def on_window_destroy(self, *args): Gtk.main_quit() def on_playpause_togglebutton_toggled(self, widget): if app.playpause_button.get_active(): img = Gtk.Image.new_from_icon_name(Gtk.STOCK_MEDIA_PLAY, Gtk.IconSize.BUTTON) widget.set_property("image", img) app.pause() else: img = Gtk.Image.new_from_icon_name(Gtk.STOCK_MEDIA_PAUSE, Gtk.IconSize.BUTTON) widget.set_property("image", img) app.play() def on_forward_clicked(self, widget): app.skip_time() def on_backward_clicked(self, widget): app.skip_time(-1) def on_progress_value_changed(self, widget): app.on_slider_seek def on_vbutton_clicked(self, widget): app.clear_playbin() app.setup_player("mediaplayer.avi") if app.playpause_button.get_active() is True: app.playpause_button.set_active(False) else: app.play() def on_ibutton_clicked(self, widget): app.clear_playbin() app.setup_player("mediaplayer.jpg") app.pause() class GstPlayer: def __init__(self): # init GStreamer Gst.init(None) # setting up builder builder = Gtk.Builder() builder.add_from_file("19_gst_player.glade") builder.connect_signals(Handler()) self.movie_window = builder.get_object("play_here") self.playpause_button = builder.get_object("playpause_togglebutton") self.slider = builder.get_object("progress") self.slider_handler_id = self.slider.connect("value-changed", self.on_slider_seek) window = builder.get_object("window") window.show_all() # setting up videoplayer self.player = Gst.ElementFactory.make("playbin", "player") self.sink = Gst.ElementFactory.make("xvimagesink") self.sink.set_property("force-aspect-ratio", True) def setup_player(self,f): # file to play must be transmitted as uri uri = "file://" + os.path.abspath(f) self.player.set_property("uri", uri) # make playbin play in specified DrawingArea widget instead of # separate, GstVideo needed win_id = self.movie_window.get_property("window").get_xid() self.sink.set_window_handle(win_id) self.player.set_property("video-sink", self.sink) def play(self): self.is_playing = True self.player.set_state(Gst.State.PLAYING) #starting up a timer to check on the current playback value GLib.timeout_add(1000, self.update_slider) def pause(self): self.is_playing = False self.player.set_state(Gst.State.PAUSED) def current_position(self): status,position = self.player.query_position(Gst.Format.TIME) return position def skip_time(self,direction=1): #skip 20 seconds on forward/backward button app.player.seek_simple(Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT, self.current_position() + float(20) * Gst.SECOND * direction ) def update_slider(self): if not self.is_playing: return False # cancel timeout else: success, self.duration = self.player.query_duration(Gst.Format.TIME) # adjust duration and position relative to absolute scale of 100 self.mult = 100 / (self.duration / Gst.SECOND) if not success: raise GenericException("Couldn't fetch duration") # fetching the position, in nanosecs success, position = self.player.query_position(Gst.Format.TIME) if not success: raise GenericException("Couldn't fetch current position to update slider") # block seek handler so we don't seek when we set_value() self.slider.handler_block(self.slider_handler_id) self.slider.set_value(float(position) / Gst.SECOND * self.mult) self.slider.handler_unblock(self.slider_handler_id) return True # continue calling every x milliseconds def on_slider_seek(self, widget): seek_time = app.slider.get_value() self.player.seek_simple(Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT, seek_time * Gst.SECOND / self.mult) def clear_playbin(self): try: self.player.set_state(Gst.State.NULL) except: pass def main(self): Gtk.main() app = GstPlayer() app.main()
GPT: v0.4 Release
JFTR: GoProTool v0.4 release
Some minor changes/fixes for the project. Recent development status is available as a (pre-)release: Release v0.4 "scarlatina".
NEW:
import files from any directory (and not just from detected SD cards) with corresponding button added to the toolbar
FIXED:
folder selection on import
progress status on video import
"FIXED":
headerbar removed from the media preview version because no images on playback
Note
This issue will probably be fixed by switching to LibVLC instead of GStreamer, see also LibVLC tutorial article.

The GSettings configuration storage system
Contents
Manage application settings with GNOME's GSettings
GSettings is the central application configuration system of the GNOME desktop. Settings are stored in binary form so you will need to use a low-level configuration tool which provides a backend for GSettings. This can be either have a graphical (dconf-editor) or commandline interface (gsettings).
In this article's example background images will be read, set and bookmarked.

Schemas
An initial configuration for an application is defined in a schema file. This XML formatted text file then will be transformed into its machine readable equivalent.
This is an example for a schema file with one property (key):
<schemalist> <schema id="org.gtk.Test" path="/org/gtk/Test/"> <key name="string-key" type="s"> <default>""</default> <summary>A string</summary> <description> Configuration key defined for a string. Default value is set to an empty string. </description> </key> </schema> </schemalist>
The nomenclature for the file is "schema.id.gschema.xml
".
The standard installation directory for schema files is /usr/share/glib-2.0/schemas
. Schema files can also be stored outside of this folder (p.e. local, for testing purposes) but these will not be shown by dconf-editor.
Now the schema files must be compiled
glib-compile-schemas /path/to/schema/files/ #default directory glib-compile-schemas /usr/share/glib-2.0/schemas/
The compiled file which is now used by GSettings is named gschemas.compiled
.
Glade
For displaying image files the GtkImage widget is required. All controls of the example app are placed in the headerbar:
"Open File" button: opens FileChooserDialog
switch: turns desktop icons on or off
"Fav" togglebutton: bookmarks file drawn in the image widget, shows whether image file is bookmarked as favourite
"Set as wallpaper" button: use file as background image
MenuButton: list of bookmarked files
Python
Load global schema
Load an existing configuration:
setting = Gio.Settings.new("full.schema.path") #load desktop background configuration setting = Gio.Settings.new("org.gnome.desktop.background")
Load local schema
If the schema file is not stored in the standard directory the location of the schemas.compiled
file must be given first:
schema_source = Gio.SettingsSchemaSource.new_from_directory(os.getcwd(), Gio.SettingsSchemaSource.get_default(), False) schema = Gio.SettingsSchemaSource.lookup(schema_source,"org.example.wallpaper-changer",False) setting = Gio.Settings.new_full(schema, None, None)
Bind widget
It is possible to directly bind GSettings properties to a widget. Property statuses then can be displayed or manipulated:
setting.bind("setting-key", widget, property, Gio.SettingsBindFlags...)
In the example app this is done with the switch widget:
self.bg_setting.bind("show-desktop-icons", self.obj("switch"), "active", Gio.SettingsBindFlags.DEFAULT)
The switch shows the current configuration status on application startup. Changes on the switch control button are applied instantly.
Get and set values
Property settings can be retrieved and defined by get_"type"
and set_"type"
. The relevant function to use is dependent on the key type, use get_string
and set_string
for strings, get_int
and set_int
for interger and so on (see PyGObject API Reference).
A property value request via get_value(key)
returnes a value of type GLib.Variant.
Therefore the set_value(key)
function also requires this datatype.
Value contents can be converted into simple datatypes:
#return string setting.get_value(key).get_string() #return anything (list, string, bool etc.) setting.get_value(key).unpack()
and vice versa:
setting.set_value(key, GLib.Variant(string_type, value)
The GNOME developer documentation provides a list of avaliable string types.
In the example app this is used to update the bookmark list:
app_setting.set_value("favourites", GLib.Variant('as',fav_list))
Listings
Schema
org.example.wallpaper-changer.gschema.xml (Source)
<?xml version="1.0" encoding="utf-8"?> <schemalist> <schema path="/org/example/wallpaper-changer/" id="org.example.wallpaper-changer"> <key name="favourites" type="as"> <default>[]</default> <summary>List of favourite wallpapers</summary> <description> Add or remove entry by pressing the 'fav' toggle button. </description> </key> </schema> </schemalist>
Glade
<?xml version="1.0" encoding="UTF-8"?> <!-- Generated with glade 3.20.0 --> <interface> <requires lib="gtk+" version="3.20"/> <object class="GtkFileFilter" id="filefilter"> <mime-types> <mime-type>image/*</mime-type> </mime-types> </object> <object class="GtkImage" id="image1"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="icon_name">emblem-favorite</property> </object> <object class="GtkMenu" id="menu"> <property name="visible">True</property> <property name="can_focus">False</property> </object> <object class="GtkApplicationWindow" id="window"> <property name="can_focus">False</property> <signal name="destroy" handler="on_window_destroy" swapped="no"/> <signal name="size-allocate" handler="on_window_size_allocate" swapped="no"/> <child> <object class="GtkImage" id="image_area"> <property name="width_request">400</property> <property name="height_request">300</property> <property name="visible">True</property> <property name="can_focus">False</property> <property name="stock">gtk-missing-image</property> </object> </child> <child type="titlebar"> <object class="GtkHeaderBar"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="title">Wallpaper changer</property> <property name="has_subtitle">False</property> <property name="show_close_button">True</property> <child> <object class="GtkButton" id="open_button"> <property name="label" translatable="yes">Open file...</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">True</property> <signal name="clicked" handler="on_open_button_clicked" swapped="no"/> </object> </child> <child> <object class="GtkBox"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="orientation">vertical</property> <child> <object class="GtkLabel"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="label" translatable="yes">Desktop icons</property> </object> <packing> <property name="expand">False</property> <property name="fill">True</property> <property name="position">0</property> </packing> </child> <child> <object class="GtkSwitch" id="switch"> <property name="visible">True</property> <property name="can_focus">True</property> </object> <packing> <property name="expand">False</property> <property name="fill">True</property> <property name="position">1</property> </packing> </child> </object> <packing> <property name="position">2</property> </packing> </child> <child> <object class="GtkMenuButton" id="fav_menu"> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">True</property> <property name="popup">menu</property> <child> <placeholder/> </child> </object> <packing> <property name="pack_type">end</property> <property name="position">1</property> </packing> </child> <child> <object class="GtkButton" id="setwp_button"> <property name="label" translatable="yes">Set as wallpaper</property> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">True</property> <signal name="clicked" handler="on_setwp_button_clicked" swapped="no"/> </object> <packing> <property name="pack_type">end</property> <property name="position">3</property> </packing> </child> <child> <object class="GtkToggleButton" id="fav_button"> <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">True</property> <property name="image">image1</property> <property name="always_show_image">True</property> <signal name="toggled" handler="on_fav_button_toggled" swapped="no"/> </object> <packing> <property name="pack_type">end</property> <property name="position">3</property> </packing> </child> </object> </child> </object> <object class="GtkImage" id="preview"> <property name="width_request">200</property> <property name="visible">True</property> <property name="can_focus">False</property> <property name="margin_right">5</property> </object> <object class="GtkFileChooserDialog" id="filechooser_dialog"> <property name="width_request">800</property> <property name="height_request">600</property> <property name="can_focus">False</property> <property name="type_hint">dialog</property> <property name="transient_for">window</property> <property name="attached_to">window</property> <property name="filter">filefilter</property> <property name="preview_widget">preview</property> <property name="use_preview_label">False</property> <signal name="delete-event" handler="on_dialog_close" swapped="no"/> <signal name="file-activated" handler="on_filechooser_dialog_file_activated" swapped="no"/> <signal name="response" handler="on_filechooser_dialog_response" swapped="no"/> <signal name="update-preview" handler="on_filechooser_dialog_update_preview" swapped="no"/> <child internal-child="vbox"> <object class="GtkBox" id="fcbox"> <property name="can_focus">False</property> <property name="orientation">vertical</property> <child internal-child="action_area"> <object class="GtkButtonBox"> <property name="can_focus">False</property> <child> <placeholder/> </child> </object> <packing> <property name="expand">False</property> <property name="fill">False</property> <property name="position">0</property> </packing> </child> </object> </child> <child type="titlebar"> <object class="GtkHeaderBar"> <property name="visible">True</property> <property name="can_focus">False</property> <property name="title">Choose image file</property> <property name="show_close_button">True</property> </object> </child> </object> </interface>
Python
#!/usr/bin/python # -*- coding: utf-8 -*- import os import sys import gi gi.require_version("Gtk", "3.0") from gi.repository import Gtk, Gio, GLib, GdkPixbuf class Handler: def on_window_destroy(self, window): window.close() def on_dialog_close(self,widget, *event): widget.hide_on_delete() return True def on_filechooser_dialog_response(self, widget, response): if response == 1: self.on_dialog_close(widget) elif response == 0: app.uri = widget.get_filename() app.draw_pixbuf(app.uri) app.handle_fav(app.uri) self.on_dialog_close(widget) def on_filechooser_dialog_file_activated(self, widget): self.on_filechooser_dialog_response(widget, 0) def on_open_button_clicked(self, widget): app.obj("filechooser_dialog").show_all() def on_setwp_button_clicked(self, widget): app.bg_setting.set_string("picture-uri", "file://{}".format(app.uri)) def on_window_size_allocate(self, widget, size): app.draw_pixbuf(app.uri) def on_filechooser_dialog_update_preview(self, widget): if widget.get_filename() != None and os.path.isfile(widget.get_filename()): pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(widget.get_filename(),200, 200, True) app.obj("preview").set_from_pixbuf(pixbuf) def on_fav_button_toggled(self,widget): if widget.get_active(): #add file to fav_list if not in list if app.uri not in app.fav_list: app.fav_list.append(app.uri) else: #remove file from fav_list if in list if app.uri in app.fav_list: app.fav_list.remove(app.uri) #update GSettings entry for favourites app.app_setting.set_value("favourites", GLib.Variant("as", app.fav_list)) #update fav list in popup menu popup = app.obj("menu") #remove all items for i in popup.get_children(): popup.remove(i) #reload all items from fav_list for fav in app.fav_list: #only label menuitem with filename instead of path item = Gtk.MenuItem(os.path.split(fav)[1]) item.connect("activate", self.on_choose_fav_from_menu, fav) popup.append(item) popup.show_all() def on_choose_fav_from_menu(self, widget, filename): app.uri = filename app.draw_pixbuf(filename) app.handle_fav(filename) class ExampleApp: def __init__(self): self.app = Gtk.Application.new("org.application.test", Gio.ApplicationFlags(0)) self.app.connect("activate", self.on_app_activate) self.app.connect("shutdown", self.on_app_shutdown) def on_app_activate(self, app): builder = Gtk.Builder() builder.add_from_file("17_gsettings.glade") builder.connect_signals(Handler()) self.obj = builder.get_object #load existing GSettings application config self.bg_setting = Gio.Settings.new("org.gnome.desktop.background") #get_value returns Gio formatted file path file = self.bg_setting.get_value("picture-uri") #convert path into string self.uri = file.get_string()[7:] #bind GSettings key to GTK+ object self.bg_setting.bind("show-desktop-icons", self.obj("switch"), "active", Gio.SettingsBindFlags.DEFAULT) #add GSettings schema from compiled XML file located in current directory (only recommended for test use, standard location: /usr/share/glib-2.0/schemas/) schema_source = Gio.SettingsSchemaSource.new_from_directory(os.getcwd(), Gio.SettingsSchemaSource.get_default(), False) schema = Gio.SettingsSchemaSource.lookup(schema_source,"org.example.wallpaper-changer", False) self.app_setting = Gio.Settings.new_full(schema, None, None) #convert value (GLib.Variant) into native list self.fav_list = self.app_setting.get_value("favourites").unpack() self.obj("window").set_application(app) self.obj("window").show_all() self.draw_pixbuf(self.uri) self.handle_fav(self.uri) def draw_pixbuf(self,file): size=self.obj("image_area").get_allocation() pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(file, size.width, size.height, True) self.obj("image_area").set_from_pixbuf(pixbuf) def handle_fav(self,uri): #set toggle button to correct state if uri in self.fav_list: self.obj("fav_button").set_active(True) else: self.obj("fav_button").set_active(False) def on_app_shutdown(self, app): self.app.quit() def run(self, argv): self.app.run(argv) app = ExampleApp() app.run(sys.argv)
Herdentrieb
Mastodon - get hype
Warnung
Dieser Beitrag wird bedarfs- und kapazitätsabhängig aktualisiert. Da sich Mastodon in massiver, aktiver Entwicklung befindet, können Teile des Inhaltes veraltet sein.
Inhalt
Was bisher geschah
Mastodon ist ein freies, verteiltes soziales Netzwerk und gleichzeitig der Name dessen Server-Komponente. Ein Mastodon-Server innhalb des gesamten Netzwerkes (Fediverse) ist eine Instanz. Die Nutzung lehnt sich im Wesentlichen an Twitter an: ein Post (Toot, dt. Tröt) enthält bis zu 500 Zeichen, die wiederum favorisiert und erneut geteilt (Boost) werden können, es gibt das Follower-Prinzip und diverse Timelines (Nutzer, lokal, öffentlich).
Was passiert gerade?
Ein junges, Open-Source-Netzwerk zieht naturgemäß primär technikaffines Volk an, gefolgt von den üblichen Early Adopter-Kandidaten. Das führt derzeit in eine inhaltliche Mischung aus Tech-Themen, darunter viel, was sich um Mastodon selbst dreht, Rollenspiele, Manga, Furries, NSFW-Content und lahme SJW-Ansprachen.
Wer bis jetzt noch nicht abgeschreckt ist, fragt sich:
Welche Instanz ist die richtige für mich?
Da prinzipbedingt jeder einen Mastodon-Server betreiben kann, tun dies auch viele. So explodiert aktuell die Zahl der Instanzen. Aber es ist eben auch damit zu rechnen, dass die Mehrheit nur kurzfristig aus Experimentierfreude existieren wird. Verschwindet eine Instanz, verschwinden auch alle Accounts (und deren Inhalte) in dieser.
Ein weiterer Punkt ist die Ausrichtung der Instanz, sei sie themenbezogen (Spaß/Memes/Aktivismus/Tech) oder lokal/landessprachlich oder der Grad an Moderation.
Hier zeichnet sich gerade eine Art Sortierungsbewegung ab: Benutzer migrieren zu Instanzen, die eher ihren Interessen bzw. ihrem Umfeld entsprechen.
Bisher ist es nicht möglich, die lokale Timeline einer Instanz ohne Registrierung anzusehen und man hier nahezu die Katze im Sack kauft. Jüngst gibt es aber Abhilfe unter
Wenn man also nicht genau eine Instanz favorisiert, weil sie thematisch passt oder man dort schon Nutzer kennt, sucht man sich am besten eine größere, allgemeine Instanz aus, bei der eine größere Chance besteht, die erste Euphoriewelle zu überleben.
Okay, ich bin dabei, was muss ich tun?
Hashtags
Tags sind die (aktuell) beste Art, bei Mastodon nach Stichwörtern zu suchen. Entweder sucht man direkt in der Suchleiste in der linken Spalte oder man ruft die URL <instance>/tags/<tag>
auf. In den Suchergebnissen werden auch Beiträge anderer Instanzen angezeigt, dies erfolgt aber nicht kongruent, man vergleiche beispielsweise den Tag #bicycle auf
User-Feed
Für jeden Nutzer wird ein Atom-Feed bereitgestellt. Leider gibt es dies nicht für Tags.
Klingt irgendwie nicht nach dem großen Twitter-Killer
Korrekt, ist es möglicherweise auch nicht. Man merkt dem Projekt definitiv an, dass es noch in den Kinderschuhen steckt. Es ist recht aufwändig, sich nach Inhalten und Usern umzusehen.
Das erinnert mich ein wenig an den "Facebook-Killer" Google+, als soziales Netzwerk hat es seine Nische gefunden, aber gerade große Medien, Blogger und die egomane Twitter-"Elite" haben die Funktionsweise von G+ nie verstanden (und wollten das auch größtenteils nicht). Damit will ich die Versäumnisse und das akkurate und stete Verschlimmbessern der Plattform seitens Google in keiner Weise in Schutz nehmen.