Springe zum Hauptinhalt

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

Listings

Python

22_editor_gtksv.py (Source)

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

import sys
import os

import gi
gi.require_version("Gtk", "3.0")
gi.require_version("GtkSource", "3.0")
from gi.repository import Gtk, GtkSource, Gio, GObject


class Handler:

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

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

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

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

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

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

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

class Editor:

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

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

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

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

        window = self.builder.get_object("app_window")
        window.set_application(app)
        window.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

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 -> figure

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


Kommentare bei

Neue Widgets in alten Glade-Dateien

Problem: deaktivierte Widgets in älteren Glade-Dateien

Es kann vorkommen, dass nach dem Öffnen einer Datei in Glade Widgets ausgegraut sind.

/images/glade_widgetsinactive.thumbnail.png

Per Tooltip wird dann zum Beispiel angezeigt:

/images/glade_widgetsinactive_tooltip.thumbnail.png

Die Ursache liegt in der angegebenen Gtk+-Version gleich am Anfang der Glade-Datei:

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

Dieses Zeile wird auch beim neuerlichen Speichern mit einer aktuellen Glade-Version nicht verändert. Abhilfe kann hier ganz simpel geschaffen werden, indem "3.0" durch eine aktuellere Version ersetzt wird (derzeit "3.20").

Stacks und Notebooks

Inhalte organisiert anzeigen

Gtk.Stack und Gtk.Notebook sind Layout-Container, die ihrerseits beliebige Widgets enthalten können.

Ein Notebook stellt ein mehrseitiges Layout mit klassischer Tab-Funktionalität zur Verfügung. Stacks bieten die gleiche Grundfunktionalität, nämlich mehrere Container innerhalb eines Widgets zu enthalten, zwischen denen man hin- und herschalten kann.

Der Hauptunterschied besteht darin, dass das Bedienelement des Stacks als separates Widget verwendet werden muss (Gtk.StackSwitcher). Verschiedene Stackswitcher können dabei auf den selben Stack zugreifen. Weiterhin lassen sich Stackswitcher in Headerbars platzieren, außerdem werden animierte Überblenden zwischen den Stack-Seiten unterstützt.

Stacks passen sich subjektiv besser in die GNOME-Umgebung ein, bieten aber nicht ganz so große Funktionalität wie Notebooks.

Das Beispiel enhält ein Fenster mit Stack, in dessen dritter Seite ein Notebook enthalten ist, das verschiedene Webseiten anzeigt.

/images/21_stacknotebook.thumbnail.png

Glade

Stack

Ein Stack, zu finden in der Sidebar unter "Container", und dessen Unterseiten lassen sich einfach in Glade erstellen und bearbeiten. Als Unterwidgets kommen im Beispiel Gtk.Image, Vte.Terminal und Gtk.Notebook zum Einsatz.

Das Stackswitcher-Widget befindet sich unter "Steuerung und Anzeige" und wird der Headerbar hinzugefügt. Es kann aber auch in reguläre Container-Widgets wie einer Box platziert und die Unterseiten horizontal oder vertikal angezeigt werden. Unter "Allgemein > Stapel" wird der Stack ausgewählt, auf den sich das Widget beziehen soll. Die anzuzeigende Seitenbezeichnung wird im jeweiligen Stack-Unterwidget unter "Packen > Titel" festgelegt. Dies funktioniert aber erst, nachdem einer Unterseite ein Widget hinzugefügt wurde. Standardmäßig ist dies zunächst leer.

Notebook

Das Notebook findet sich ebenfalls unter "Container". Die Steuerungseinheit des Tabs ist ein bei Erstellung einer Seite generiertes Label-Child-Widget. Als Container-Widgets der Unterseiten werden hier Gtk.ScrolledWindows verwendet. Diese benötigt man auch z.B. für die Anzeige von (längeren) Tabellen (siehe auch Artikel zu List-/TreeStores Nr. 1 und Nr. 2).

Die Tab-Leiste des Notebooks bietet die Möglichkeit, sowohl am Anfang als auch am Ende ein Container-Widget bereitzustellen (unter "Allgemein > Start-Aktion/End-Aktion"), in dem zum Beispiel feste Buttons untergebracht werden können. Im Beispiel wird am Anfang ein "Home"-Button eingerichtet.

Python

Für das Umherschalten zwischen Stack-Unterseiten und Notebook-Tabs werden keine Signale benötigt. Im Beispiel werden nur zwei Signale benötigt, einmal für das Abfangen des "exit"-Kommandos innerhalb des Terminals und für den Button in der Notebook-Tableiste.

WebKit2

Die Darstellung von Webseiten wird im Beispiel von WebKit2 erledigt. Das zentrale Modul dabei ist WebKit2.WebView. Ein neues WebView-Objekt selbst ist bereits ein scrollbares Gtk+-Widget in einem Gtk.Viewport, muss also laut API-Referenz nicht mehr in ein Gtk.ScrolledWindow platziert werden. Dies funktionierte im Test zwar für Gtk.Stack, nicht aber für Gtk.Notebook, weshalb dort trotzdem als "Unterlage" ein ScrolledWindow-Widget verwendet wird.

Das WebView-Widget wird nach folgendem Muster erstellt:

# 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

21_stacknotebook.py (Source)

#!/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>

Kommentare bei

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 und github_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.

/images/non/non_term.gif

Mediaplayer mit VLC

Mediaplayer mit LibVLC realisieren

VLC ist nicht nur ein Multimediaplayer, sondern auch ein Framework, zu dem Python-Bindings verfügbar sind. In diesem Beispiel wird analog zum GStreamer-Artikel ein einfacher Mediaplayer mittels LibVLC umgesetzt.

/images/20_vlc_player.thumbnail.png

LibVLC

Voraussetzung für die Verwendung ist die Installation der Python-Bindings. Diese sind unter der Paketbezeichnung python-vlc zu finden.

Glade

  • Darstellungsbereich der Mediendatei: Widget Gtk.DrawingArea

  • Steuerungselemente: Vor-/Zurückspulen (Gtk.Button), Pause (Gtk.Togglebutton)

  • Medienauswahl: Buttons, um Video- oder Bilddatei anzuzeigen

  • Playback manipulieren: Buttons zum Stummschalten und Drehen des Videos

Python

Player einrichten

Der VLC-Player wird initiiert, sobald das dazugehörige Widget, in diesem Fall also Gtk.DrawingArea gezeichnet wird. Dazu wird das Signal realize genutzt, das grundsätzlich für die Klasse der Widgets verfügbar ist.

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)

Als Optionen können Kommandozeilenoptionen von VLC übergeben werden. Im Beispiel wird beim Klick auf den "Rotate"-Button das Bild um 180° gedreht. Der Player wird erneut initiiert und die zusätzliche Option --video-filter=transform{type=180} übergeben.

Medium abspielen

Wie auch der GStreamer-Player kann der VLC-Player viele Video-/Audio- oder Bild-Formate anzeigen bzw. abspielen.

player.set_mrl(file_url)
# Datei abspielen
player.play()
# Pause/Play-Schalter
player.pause()

Positionsanzeige

Die Umsetzung des Fortschrittsbalkens und die Nutzung als Schiebereglers gestaltet sich ziemlich einfach.

# Position abfragen
player.get_position()
# Position bestimmen
player.set_position(val)

Der Wertebereich liegt dabei zwischen 0 und 1. Das Problem bei diesen Funktionen ist, dass sie relativ ressourcenintensiv arbeiten und das Playback mitunter verruckelt ist. Die Lösung im hiesigen Beispiel besteht darin, get_position-Abfragen zu umgehen, indem die Regler-Position herangezogen wird.

Möglichkeiten und Limitierungen

Die Nutzung der LibVLC-Python-Bindings erweist sich als einfach und angesichts der GStreamer-Umsetzung als geradezu intuitiv. Auch das "Headerbar-Problem" besteht nicht.

Auf der anderen Seite greift man hier auf großes Projekt zurück, man muss VLC und die Python-Bindings installiert haben anstatt einfach das GStreamer-Modul aus dem GObject Introspection-Repository zu verwenden. Auch ist im Test der Ressourcenverbrauch von VLC gegenüber GStreamer größer.

Listings

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)

Glade

20_vlc_player.glade (Source)

<?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>

Mediaplayer mit GStreamer

Mediaplayer mit GStreamer 1.x realisieren

GStreamer ist ein Multimedia-Framework, das zum Anzeigen und (De-)Kodieren von Mediendateien verwendet werden kann.

/images/19_gst_player.thumbnail.png

Glade

  • Darstellungsbereich der Mediendatei: Widget Gtk.DrawingArea

  • Steuerungselemente: Vor-/Zurückspulen (Gtk.utton), Pause (Gtk.Togglebutton)

  • Medienauswahl: Buttons, um Video- oder Bilddatei anzuzeigen

Python

Player einrichten

Elemente und Pipelines

GStreamer handhabt alle möglichen Arten von Medienflüssen. Jeder Schritt in dieser Verarbeitungskette wird per Element definiert und in Pipelines verbunden. Eine solche Pipeline besteht typischerweise aus "source"-, "filter"-/"decode"- und "sink"-Elementen.

------------------------------------------------------
|  Pipeline                                          |
|                                                    |
|  -------------   ----------------   -------------- |
|  | source    |   | filter       |   | sink       | |
|  |           |->>| decoder      |->>|            | |
|  | Quelle    |   | Verarbeitung |   | Ausgabe    | |
|  -------------   ----------------   -------------- |
------------------------------------------------------

Nach diesem Prinzip wird dies mittels Gst-Modul umgesetzt:

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

Fertige Pipelines

Es besteht auch beispielsweise die Möglichkeit, Audio- und Videosignale voneinander getrennt werden, indem jeweils ein "videosink" und ein "audiosink" erstellt usw. Auf der anderen Seite gibt es vorgefertigte Pipelines für Standardaufgaben wie etwa das Abspielen von Medien. Ein solches Element ist "playbin", das den Code signifikant vereinfacht:

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)

Und los!

Eine Pipeline oder ein "playbin"-Element können nun über Gst.STATE gesteuert werden:

player.set_state(Gst.State.PLAYING)
player.set_state(Gst.State.PAUSED)

Fortschrittsanzeige

Die Fortschrittsanzeige ist an dieser Stelle keine Gtk.ProgressBar sondern eine horizontale GtkScale. Mit diesem Widget lässt sich nicht nur eine Position anzeigen, sondern auch per Maus setzen. Für letzteres wird das Signal value-changed benötigt. Streng genommen ist das Signal change-value an dieser Stelle die sauberere Lösung, die im nachfolgenden Beitrag zur Umsetzung des Mediaplayers mit LibVLC verwendet wird.

Möglichkeiten und Limitierungen

Bei der Einarbeitung in GStreamer stolpert man (an dieser Stelle generalisiert die Autorin weitgehend und möglicherweise unbegründet) über diverse Hürden:

Es gibt eine Reihe von Tutorials. Die Umsetzung wird durch zwei Umstände erschwert:

  1. Die primäre Sprache von und mit GStreamer ist C. Mit Python steht man eher auf experimentellem Boden.

  2. Durch die Versionssprünge sowohl bei GStreamer (von 0.10 auf 1.x) als auch Python (2.x auf 3.x) funktionieren viele ältere Anleitungen nicht mehr ohne weiteres.

Es gibt weiterhin Effekte, die sich nicht erschließen. Das in diesem Artikel aufgeführte Beispiel funktioniert nicht, wenn das Fenster eine Headerbar enthält. Des Weiteren ist die Videodarstellung unter Wayland fehlerhaft (Audio läuft). Beide Probleme sind mit der Verwendung von "gtksink" lösbar.

Listings

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

Glade

19_gst_player.glade (Source)

<?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>

GPT: v0.4 Release

JFTR: v0.4 des GoProTools veröffentlicht

Das GoProTool hat ein wenig Zuneigung bekommen. In diesem Zuge habe ich aus dem aktuellen Stand den Release v0.4 "scarlatina" erstellt.

NEU:

  • Import von jedem Verzeichnis aus (vorher nur von erkannten Speicherkarten) mit praktischen Buttons in der Toolbar

REPARIERT:

  • Ordnerauswahl beim Import

  • Fortschrittsanzeige bei Videoimport

"REPARIERT":

  • Headerbar in der Vorschau-Version entfernt, weil Bild bei Playback nur funktioniert, wenn das Fenster keine Headerbar enthält (fragt nicht - isso, weil isso)

Bemerkung

Dies wird voraussichtlich beim Wechsel von GStreamer zu LibVLC behoben, siehe auch LibVLC-Artikel.

/images/window_player_v0.4.thumbnail.png

Das Konfigurationssystem GSettings

Das GNOME-eigene Konfigurationssystem GSettings

GSettings ist GNOMEs zentrales Konfigurationssystem für Anwendungen. Es ist die Schnittstelle für verschiedenmögliche Backends, gemeinhin ist dies dconf.

Mittels grafischem (dconf-editor) oder Kommandozeilentool (gsettings) lassen sich Konfigurationen abfragen und manipulieren.

Das folgende Beispiel kann Hintergrundbilder laden, festlegen und bookmarken/favorisieren.

/images/17_gsettings.thumbnail.png

Schemas

Um eine Konfiguration für eine Anwendung zu erstellen, muss diese in einer Schema-Datei definiert werden. Diese Datei ist eine XML-formatierte Datei, die anschließend in sein maschinenlesbares Äquivalent überführt werden muss.

Ein Beispiel für eine Schema-Datei mit einer festzulegenden Eigenschaft (key) wäre etwa:

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

Die Dateibenennung folgt der Vorgabe "schema.id.gschema.xml". Das Standardinstallationsverzeichnis für Schema-Dateien ist /usr/share/glib-2.0/schemas. Schema-Dateien können auch außerhalb dieses Verzeichnisses genutzt werden (z.B. lokal, zu Testzwecken), sie werden dann aber nicht vom dconf-editor angezeigt.

Die erforderliche Kompilierung erfolgt mit

$ glib-compile-schemas /path/to/schema/files/
$ # default directory
$ glib-compile-schemas /usr/share/glib-2.0/schemas/

Die kompilierte und nun von GSettings verwendete Datei ist gschemas.compiled.

Glade

Das Beispiel soll Bilder anzeigen, dafür wird das Widget GtkImage benötigt. Alle Steuerungselemente werden in der Headerbar untergebracht:

  • "Open File"-Button: öffnet einen FileChooserDialog

  • Switch: schaltet Desktop-Icons an oder ab

  • "Fav"-Togglebutton: bookmarkt angezeigte Dateien, zeigt an, ob angezeigte Datei als Favorit markiert ist

  • "Set as wallpaper"-Button: angezeigte Datei als Hintergrundbild verwenden

  • MenuButton: unterhalb des Buttons wird eine Liste der favorisierten Dateien angezeigt, die von dort aus aufgerufen werden können

Python

Globales Schema laden

Eine bestehende Konfiguration zu laden, geschieht einfach per

setting = Gio.Settings.new("full.schema.path")
# load desktop background configuration
setting = Gio.Settings.new("org.gnome.desktop.background")

Lokales Schema laden

Bei einem lokal gespeicherten Schema muss der Ort der schemas.compiled angegeben werden, bevor die Konfiguration geladen werden kann:

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)

Widget verknüpfen

Es ist möglich, GSettings-Eigenschaften direkt an Widgets zu binden. Diese können dann bidirektional Zustände anzeigen bzw. man kann Änderungen an ihnen vornehmen:

setting.bind("setting-key", widget, property, Gio.SettingsBindFlags...)

Im Beispiel wäre dies also

self.bg_setting.bind("show-desktop-icons", self.obj("switch"), "active", Gio.SettingsBindFlags.DEFAULT)

Der Schalter zeigt beim Start die aktuelle Einstellung an. Eine Änderung des Status ist sofort wirksam.

Werte abrufen und festlegen

Eigenschaften können mit get_"type" und set_"type" ermittelt und festgelegt werden. Die relevante Funktion ist vom festgelegten Schlüsseltyp abhängig, also get_string und set_string für Zeichenketten, get_int und set_int für Ganzzahlen usw. (siehe PyGObject API Reference).

Wird der Wert einer Eigenschaft per get_value(key) abgefragt, wird dies immer als Wert des Typs GLib.Variant zurückgegeben. Entsprechend erwartet die Funktion set_value(key) ebenfalls diesen Typ.

Die Inhalte dieser Werte lassen sich einfach in simple Datentypen konvertieren, z.B.

# return string
setting.get_value(key).get_string()
# return anything (list, string, bool etc.)
setting.get_value(key).unpack()

Umgekehrt lassen sich reguläre Datentypen nach folgendem Muster als GLib.Variant-Typ ausdrücken und an GSettings übergeben:

setting.set_value(key, GLib.Variant(string_type, value)

Eine Liste der verfügbaren Stringtypen finden sich in der GNOME Developer-Dokumentation.

Im Beispiel wird auf diese Art die Favoritenliste aktualisiert:

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>

Python

17_gsettings.py (Source)

#!/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)

Glade

17_gsettings.glade (Source)

<?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>

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.

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.