Springe zum Hauptinhalt

NoN: Fortschritte

Knights of Ni - Fortschritte

Wie bereits erwähnt, bastel ich aktuell an einer GUI für Nikola, die einige Fortschritte vorzuweisen hat:

  • Titelleiste entfernt, da Headerbar benutzt wird

  • Wechseln zwischen Nikola-Instanzen (zuletzt benutzte wird gespeichert)

  • Bookmarkfunktion (bislang können nur Bookmarks hinzugefügt werden, bei Bedarf manuell aus der config entfernen)

  • Untersützung für mehrsprachige Seiten:

  • Anzeige der Standardsprache und der konfigurierten anderen Sprachen

  • Posts/Pages-Tab zeigt Vorhandensein von Übersetzungen an

  • Tab "Translations" zeigt vorhandene Dateien an

  • per Rechtsklick können neue Übersetzungen angelegt werden, dabei wird der Ausgangsbeitrag gemäß entsprechendem Dateimuster gespeichert

  • ich weiß noch nicht, wie sinnvoll dieser separate Tab ist (Redundanz) und ob ich ihn beibehalte

  • hat der Artikel keinen Titel, wird das interne Kürzel (slug) oder, falls ebenfalls nicht vorhanden, der Dateiname angezeigt

  • gesprächiges Log, um vorzutäuschen, dass ganz viel wichtiger Kram passiert

/images/non/non2.thumbnail.png

Das läuft aus meiner Sicht schon alles erstaunlich gut. Dummerweise habe ich bei der Verwendung von Glade (und auch Python, aber vor allem Glade) inzwischen ebenfalls einige Fortschritte gemacht, was mich ernsthaft zur Überlegung geführt hat, das GoProTool nochmal zu überarbeiten. Die Oberfläche würde ich belassen, aber viel Funktionalität könnte effizienter umgesetzt werden. Ich stelle das mal hinten an.

Geplant ist nun ein simples Nikola-unabhängiges Vorlagensystem und anschließend ist etwas Fleißarbeit bei der Lokalisation erforderlich.

Artikelübersetzungen

Sobald ich herausgefunden habe, wie Multilingualität in Nikola funktioniert, werde ich die Tutorialartikel ins Englische übersetzen.

Nachtrag: okay, war war einfach...

YouNiversity: Python Tricks

Daniel Bader zeigt auf seinem YouTube-Kanal allerlei Nützliches zum Thema Python.

Aktuell gibt es sein in Arbeit befindliches Buch "Python Tricks. The Book" gerade zum unschlagbaren Preis von 9 $ (+ 1,71 $ deutsche Umsatzsteuer): Klick

Es bestehen keine persönlichen oder/und finanziellen Verbindungen.

Dialoge

Anzeige von Dialogfenstern

Dialoge sind ergänzende Fenster zur Anwendung und dienen der Interaktion mit dem Benutzer, in denen Informationen angezeigt werden oder Eingaben vom Benutzer abgefragt werden können. Die GtkDialog-Klasse bietet einige Unterklassen für gebräuchliche Anzeigen und Abfragen, wie die im Beispiel verwendeten AboutDialog- und MessageDialog (Artikel zum FileChooserDialog).

/images/13_dialoge.thumbnail.png

Glade

Dialog-Widgets findet man unter "Oberste Ebene" neben den Fenster-Widgets.

Dialoge sind ergänzende Fenster, um den Fokus des Nutzers zu lenken. Sie können direkt an ein übergeordnetes Fenster angeheftet werden, mindestens aber müssen sie unter "Allgemein > Fensterattribute > Vorübergehend für:" einem Eltern-Fenster zugeordnet werden. Sie erscheinen dadurch nicht als separates Fenster in der Übersicht und erben ein vorhandenes Icon.

AboutDialog

Das "About"-Dialogfenster bietet in der Regel Informationen zum Projekt, darunter Version, Lizenz, beteiligte Programmierer, Übersetzer etc. Dies alles lässt sich sehr einfach direkt in Glade angeben.

MessageDialog

Der MessageDialog ist ein Standarddialog zum Anzeigen oder Abfragen von Informationen. Er ist so konfiguriert, dass er keine eigene Fensterdekoration besitzt und nicht als Fenster in der Taskbar erscheint. Außerdem bietet er die Möglichkeit, Standardbuttons einzurichten.

Buttons und Responses

Dialoge verfügen bereits intern über eine Gtk.ButtonBox, die mit beliebigen Buttons befüllt werden kann. Dieser Bereich ist als "intern action_area" gekennzeichnet.

Im Gegensatz zu Buttons in normalen Fenstern müssen in Dialogen keine Signale auf clicked angelegt werden, sondern man legt in den Button-Eigenschaften unter "Allgemein" eine Antwortkennung (Response) fest (int) und belegt das Signal response des GtkDialog.

Standardbuttons wie im MessageDialog auswählbar besitzen vorgegebene Response-Kennungen (siehe Python GI API Reference):

  • Ok -5

  • Abbrechen -6

  • Schließen -7

  • Ja -8

  • Nein -9

  • [X] -4

Der große Vorteil der Responses besteht darin, dass sie sich direkt auf das Dialog-Objekt beziehen; man kann die Responses in einer Funktion verarbeiten und muss dies nicht für jeden einzelnen Button vornehmen.

Wiederherstellbare Dialoge

Das Problem von per destroy-Signal geschlossenen Fenstern besteht darin, dass sie sich nicht wieder aufrufen lassen. Deshalb wird stattdessen das Signal delete-event belegt.

Python

Dialog aufrufen

Da Dialoge auch Gtk.Windows sind, lassen sie sich mit show_all() aufrufen. Die Funktion von Dialogen besteht allerdings in der Regel darin, Nutzereingaben zu erfassen oder Informationen zu vermitteln. Deshalb ruft man die Fenster am besten mit run() auf. Dies bewirkt, dass das Dialogfenster über dem Elternfenster fixiert wird und jenes nicht aktiv ist, bis ein Response-Signal ausgeführt wird.

Responses

Beim Auslösen des response-Signals wird die Antwortkennung als Parameter übergeben, so kann, wie bereits erwähnt, jede Kennung innerhalb einer einzelnen Funktion verarbeitet werden:

def on_dialog_response(self, widget, response):
    if response == 0:
        widget.hide_on_delete()
    elif response == 1:
        do.something()
    elif response == (2 or 3):
        do.something.different()

Delete-event

Mit der Funktion hide_on_delete() ausgeblendete Dialoge oder reguläre Fenster lassen sich mit show_all() wieder anzeigen:

def on_dialog_delete_event(self, widget, event):
    widget.hide_on_delete()
    return True

Mehrere Glade-Dateien

Wie bereits erwähnt, können mehrere Dateien für Fenster und Dialoge innerhalb eines Projektes verwendet werden. Allerdings ist es nicht möglich, diese dateiübergreifend aneinanderzubinden. Hierzu wird die set_transient_for-Funktion von Gtk.Window benötigt:

dialog.set_transient_for(mainwindow)

Die Zugehörigkeit zum Elternwidget wird in Glade in den Eigenschaften unter "Allgemein > Vorübergehend für:" angegeben.

Weiterlesen…

Ansichtssache

Daten anzeigen mit TreeStore

(Fortsetzung zum ListStore-Artikel)

/images/12_treestore.thumbnail.png

TreeStore vs. ListStore

Im Gegensatz zum ListStore können Zeilen eines TreeStores ihrerseits Kind-Elemente besitzen, die append-Funktion benötigt demzufolge ein weiteren Parameter, der einen Bezug zu einer anderen Datenzeile anzeigt:

#append row to liststore
store.append([value1, value2, value3])

#append row to treestore
store.append(parent, [value1, value2, value3])

Der Wert der Variable parent ist entweder

  • None, wenn die Zeile keine übergeordnete Zeile besitzt, oder

  • TreeIter, der zur übergeordneten Zeile zeigt

Der TreeIter wird beim Erstellen einer Zeile erzeugt, untergeordnete Zeilen werden nach folgendem Schema angelegt:

row1 = store.append(None, [value1, value2, value3])
row2 = store.append(row1, [value1, value2, value3])

Man erhält den TreeIter-Wert einer Zeile am einfachsten über die get_selection-Funktion des Gtk.TreeSelection-Widgets von TreeView (wird automatisch angelegt).

Glade

Im Beispiel werden zwei TreeStores und die jeweils enthaltenen Spalten angelegt, dazu die TreeView-Widgets zur Anzeige.

TreeModelSort

Spalten lassen sich mit der Funktion set_sort_column_id einfach sortieren. Wendet man diese Funktion direkt auf TreeStore an, werden logischerweise alle TreeView-Widgets, die darauf zurückgreifen, sortiert.

Für diese Fälle muss man TreeModelSort-Elemente "zwischenschalten", d.h. man erstellt aus der Widget-Seitenleiste unter "Sonstiges > Sortierung für Baumansichtsmodell" (4. Eintrag) ein Widget und weist ihm den gewünschten TreeStore zu (einzige Option unter "Allgemein"). Anschließend ersetzt man im TreeView das Modell mit dem eben erstellten TreeModelSort.

Die Sortierungsfunktion führt man wie zuvor, nur auf das TreeModelSort-Objekt, aus.

TreeModelFilter

TreeModelFilter ermöglicht die Darstellung bestimmter Zeilen, in Glade wird wie bei TreeModelSort verfahren, zuerst das Element anlegen (3. Eintrag unter "Sonstige"), anschließend erfolgen die Zuweisungen zum Modell und TreeView.

Im gewählten Beispiel sollen Sorten nach der Fruchtfarbe sortiert werden, es wird also noch ein Container für Buttons benötigt, also eine GtkButtonBox.

Formatierung aus dem Modell laden

Neben den anzuzeigenden Spalten gibt es im ersten TreeStore eine Spalte "weight". Der Wert in dieser Spalte wird dazu verwendet, die Zelle in Fettschrift darzustellen. Dazu wird in den Eigenschaften des CellRenderers unter Schriftgewicht die entsprechende Spalte angegeben (der Wert für normale Schrift ist 400). Analog dazu können beispielsweise auch Zellen eingefärbt oder weitere Schriftformatierungen vorgenommen werden.

Python

TreeModelSort

Durch die Positionsabfrage von GtkTreeSelection.get_selected() erhält man ein Tupel (model,pos), pos von model zeigt dabei auf TreeModelSort (bzw. analog auf TreeModelFilter), nicht auf TreeStore und erfordert eine Konvertierung:

model, pos = selection.get_selected()
converted_iter = treesort.convert_iter_to_child_iter(pos)
store.set_value(converted_iter, column, value)

TreeModelFilter

Zunächst muss eine Filterfunktion erstellt werden, in der die Sichtbarkeit von Zeilen definiert wird, im Beispiel also die Variable self.color:

def color_filter_func(self, model, iter, data):
    if model[iter][2] == self.color:
        return True
    else:
        return False

Die Funktion wird zunächst nach dem Schema

treefilter.set_visible_func(filter_func)

zugewiesen, jede Filterung wird dann per refilter() ausgelöst, also wenn das Button-Signal ausgelöst wird:

def on_button_clicked(self, widget):
    x.color = widget.get_label()
    x.obj("treefilter").refilter()

Weiterlesen…

Neues Projekt: Knights of Ni

Knights of Ni - kleiner Manager für den statischen Webseitengenerator Nikola

/images/non/non_window.thumbnail.png

Neues kleines GitHub-Übungsprojekt aus Glade und Python zusammengezimmert: Knights of Ni.

Inwiefern ich das weiter ausbaue, entscheide ich operativ; jetzt widme ich mich erstmal wieder weiter den Tutorial-Artikeln.

Exterminate!

Das VTE-Terminal-Widget

/images/11_terminal.thumbnail.png

Glade

Das Widget findet man in der Widget-Seitenleiste ganz unten und stellt ein fertiges Terminal bereit. Um das Terminal auf exit zu schließen, muss das Signal child-exited abgefangen werden.

Ein Klick auf den Button soll innerhalb dieses Terminals eine Python-Konsole starten, hier wird also das clicked-Signal belegt.

Python

Elemente außerhalb des Gtk-Moduls, die mit Glade verwendet werden, müssen als GObject-Typ registriert werden (dies betrifft beispielsweise auch das GtkSource.View-Widget (Modul GtkSource):

GObject.type_register(Vte.Terminal)

Das Terminal wird mit der Funktion spawn_sync initiiert, die diverse Parameter erwartet. Die Dokumentation liefert Details, für eine einfache Bash kommt man mit viel Defaults und Nones aus:

terminal.spawn_sync(
        Vte.PtyFlags.DEFAULT,
        None,
        ["/bin/bash"],
        None,
        GLib.SpawnFlags.DEFAULT,
        )

Um eine Eingabe an die Konsole zu schicken, bedarf es der Funktion feed_child. Als Parameter muss der auszuführende Befehl als UTF-8-kodierter String inklusive newline, also dem "Enter" übergeben werden:

command = "python\n"
x.terminal.feed_child(command.encode())

Die Ausgabe ins Terminal kann mit der Funktion get_text() abgefangen werden. Die Funktion gibt ein Tupel zurück, dessen erstes Element der Ausgabestring ist. Dieser enthält allerdings den gesamten Terminalinhalt, also auch viele Leerzeilen, die sich mit herkömmlichen String-Operationen beseitigen lassen.

widget.get_text()[0].rstrip()

Weiterlesen…

Romani ite domum

Lokalisation mit gettext und locale

/images/10_lokalisation.thumbnail.png

Glade

Strings von Labels oder Menüs sind standardmäßig als übersetzbar konfiguriert (Checkbox unter "Beschriftung"), insofern muss hier nichts weiter beachtet werden. Glade-Projektdateien werden direkt von GetText verarbeitet.

Python

Übersetzbare Strings

Zur Übersetzung freigegebene Strings werden durch eine Einklammerung mit vorausgehendem Unterstrich markiert und beim Aufruf von xgettext erkannt:

_ = gettext.gettext
translatable_string = _("translate me")

(bind)textdomain einrichten

Nun muss man Python noch zeigen, unter welchem Namen und Pfad die MO-Dateien (siehe unten) zu finden sind:

locale.bindtextdomain(appname, locales_dir)
locale.textdomain(locales_dir)
gettext.bindtextdomain(appname, locales_dir)
gettext.textdomain(appname)
builder.set_translation_domain(appname)

set_translation_domain muss vor dem Laden der Glade-Datei(en) aufgerufen werden.

GetText

POT

POT steht für Portable Object Template und ist dem Namen zufolge die Vorlage für Übersetzungen. Diese Datei enthält alle übersetzbaren Strings. Nachdem eine leere POT-Datei erstellt wurde, ruft man nun xgettext nach folgendem Muster für alle Quelldateien auf:

$ xgettext --options -o output.pot sourcefile.ext

Die erkannten Strings werden nach dem Schema

#: sourcefile.ext:line number
msgid "translatable string"
msgstr ""

der angegebenen POT-Datei hinzugefügt. Die Markierung der Fundstelle(n) des Strings kann mit der Option --no-location verhindert werden.

Für das Beispiel wird also je ein Aufruf für die Glade- und Python-Datei benötgt:

$ xgettext --sort-output --keyword=translatable --language=Glade -j -o 10_localization/TUT.pot 10_lokalisation.glade
$ xgettext --language=Python -j -o 10_localization/TUT.pot 10_lokalisation.py

Mit der Option -j (--join-existing) wird eine bestehende Datei um zusätzliche Strings ergänzt und funktioniert deshalb sowohl bei der Initiierung (vorher einfach mit touch template.pot die leere Datei erstellen) als auch bei erneutem Aufruf zum Aktualisieren neuer Strings.

PO

Die übersetzten Strings werden in jeweils einer PO-Datei gespeichert. Eine neue Übersetzung legt man mit

$ msginit --input=source.pot --locale=xx
# xx=language code

an, das eine PO-Datei mit dem Namen xx.po (z.B. de.po) anlegt. Diese kann direkt im Texteditor oder mittels Tools wie PoEdit bearbeitet werden. Die deutschsprachige Lokalisation wird also angelegt mit

$ msginit --input=TUT.pot --locale=de

Wird die POT-Datei verändert, kann man die PO-Dateien mit msgmerge abgleichen und anschließend die neuen Strings übesetzen:

$ msgmerge lang.po template.pot > new_lang.po

MO

MO-Dateien sind auf Maschinenlesbarkeit optimierte PO-Dateien und letztlich die, die vom Programm benutzt werden. Unterhalb der angegebenen bindtextdomain liegen die Lokalisationsdateien nach der Verzeichnisstruktur (path/to/bindtextdomain)/locale/language code/LC_MESSAGES/appname.po

Im Beispiel wird die bindtextdomain einfach im lokalen Verzeichnis angelegt, die erzeugte de.po wird mit msgfmt in die MO-Datei überführt:

$ msgfmt --output locale/de/LC_MESSAGES/TUT.mo de.po

Tipps

xgettext-Optionen

--no-location

Ausgabe der Zeilennummer(n) und Datei (als Kommentar) des Strings verhindern

--omit-header

Überschreiben der Header-Informationen verhindern

--sort-output

Alphabetische Sortierung der Strings

Obsolete Strings entfernen

Strings, die aus der POT entfernt werden, bleiben in den Übersetzungen erhalten. Dies lässt sich durch den Aufruf von

$ msgattrib --set-obsolete --ignore-file=PRJ.pot -o xx.po xx.po

beheben.

Weiterlesen…

Überlistet

Daten in ListStore speichern und mit ComboBox und TreeView anzeigen

Für die Speicherung und Anzeige von Daten in Listen- oder Tabellenform benötigt man in GTK+-Anwendungen verschiedene Elemente:

  1. Im Modell werden die Daten verwaltet, es gibt zwei Typen:

    • ListStore: flache Liste, die Spalten können neben Text-, Zahlenwerten auch GTK+-Elemente (z.B. Buttons, Checkboxen) enthalten

    • TreeStore: funktioniert prinzipiell wie ListStore, Zeilen können ihrerseits Kind-Einträge besitzen, Daten können im Gegensatz zu ListStore nicht in Glade angegeben werden (TreeStore-Artikel)

  2. Widgets:

    • TreeView: dieses Widget eignet sich zum Anzeigen, Sortieren, Bearbeiten von Daten, wird von beiden Modelltypen verwendet; es können parallel mehrere TreeView-Widgets angelegt werden, die auf dieselbe Datenbasis (Modell) zurückgreifen, aber zum Beispiel verschiedene Spalten anzeigen

    • ComboBox: Comboboxen dienen der Auswahl aus einer gegebenen Liste, deren Datenbasis ein List- oder TreeStore sein kann (siehe Artikel zu Spinbutton und Combobox)

    • CellRenderers: Unterwidgets, in denen die anzuzeigenden Daten, deren Layout und weitere Optionen wie Bearbeitbarkeit festgelegt werden

/images/09_treestore2.thumbnail.png

Glade

ListStore

Um die Vielseitigkeit von ListStore zu skizzieren, wird im Beispiel ein Gtk.ListStore (zu finden in der Elementauswahl links unter "Sonstiges > Listenverwahrung") erstellt und von drei Widgets verwendet.

Zunächst werden ein paar Spalten erstellt. ListStore-Daten lassen sich direkt in Glade eingeben. Dies ist allerdings nur für wenige Zeilen und Spalten praktikabel und übersichtlich. Selbst wenige Daten würde ich immer direkt im Python-Code einlesen.

Wie man sieht, werden Änderungen im ListStore (Sortierung, Inhalt) sofort in allen Widgets aktualisiert, die auf dieses Objekt zugreifen. Für verschiedene Sortierungen des selben List-/TreeStores muss man Gtk.TreeModelSort anwenden (Beispiel siehe TreeStore-Artikel).

/images/09_treestore1.thumbnail.png

Widgets

ComboBox

Als "Baumansichtsmodell" wird wie auch bei den folgenden Widgets der ListStore ausgewählt. Über "Edit > Hierarchie" ein CellRendererText hinzugefügt. Im ersten Feld ("Text") stellt man ein, aus welcher Spalte das Dropdown-Menü angezeigt werden soll. Um die Auswahl zu verarbeiten, wird das Signal changed belegt.

TreeView #1

Das erste TreeView-Widget wird innerhalb eines Gtk.ScrolledWindow-Containers angelegt. Wie bei ComboBox werden nun beliebige CellRenderer angelegt. Wird der Sortierungsanzeiger aktiviert, können die Spalten mit Klick auf den Spaltenkopf sortiert werden. In der Sortierspaltenkennung wird die Spalte angegeben, nach der sortiert werden soll, auf diese Weise kann man eine Spalte auch gemäß einer anderen Spalte sortieren (hier im Beispiel wird die mittlere Spalte nach der letzten sortiert, die Sortierung der beiden hinteren Spalten liefert also das gleiche Ergebnis.

TreeView #2

Das zweite TreeView-Widget wird innerhalb eines Sichtfeldes (Gtk.Viewport) erstellt. Dieser Container bietet keine Scrollbalken, das Widget vergrößert automatisch, so dass alle Zeilen sichtbar sind. Bei größeren Tabellen ist ein ScrolledWindow also praktikabler. Es werden die gleichen Daten angezeigt wie zuvor, allerdings ohne Sortierungsanzeiger, dafür wird die mittlere Spalte ("Description") editierbar gemacht und erhält eine Funktion für das Signal edited.

Button

Ein Klick auf den Button soll jeweils eine weitere Zeile zum ListStore hinzufügen, es wird also das clicked-Signal belegt.

Python

TreeStore

Die in TreeStore vorhandenen Zeilen lassen sich einfach über for row in store abrufen. Neue Zeilen lassen sich mit append hinzufügen, andere Optionen wären insert oder remove, um Zeilen an bestimmten Positionen einzufügen oder zu entfernen.

ComboBox

Normalerweise benötigt man für den Zugang zu einer Datenzeile einen TreeIter, das Objekt, das auf den Pfad im Modell zeigt (alternativ kann man diese auch über TreePath ansprechen).

iter, model = widget.get_active_iter(), widget.get_model()
row = model[iter]
print("Selection:", row[0])

Zellen bearbeiten

Das edited-Signal übergibt als Parameter die bearbeitete Zeile und den neuen Zelleninhalt. Dieser muss allerdings explizit als neuer Zelleninhalt übergeben werden, sonst zeigt die Zelle nach der Bearbeitung wieder den alten Inhalt an. Dafür kann man einfach die vom Widget übergebene Position (TreePath) statt des TreeIters verwenden.

def on_cellrenderer_descr_edited(self, widget, pos, edit):
    x.store[int(pos)][1] = edit

Listings

Python

09_liststore.py (Source)

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

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


class Handler:

    def on_window_destroy(self, *args):
        Gtk.main_quit()

    def on_cbox_changed(self, widget):
        iter, model = widget.get_active_iter(), widget.get_model()
        row = model[iter]
        print("Selection:", row[0])

    def on_cellrenderer_descr_edited(self, widget, pos, edit):
        x.store[int(pos)][1] = edit

    def on_add_row_button_clicked(self,widget):
        x.store.append(list(x.more_rows[len(x.store) - 3]))
        #set button inactive when all rows are appended
        if len(x.store) == 7:
            x.button.set_sensitive(False)


class Example:

    def __init__(self):

        self.builder = Gtk.Builder()
        self.builder.add_from_file("09_liststore.glade")
        self.builder.connect_signals(Handler())

        window = self.builder.get_object("window")
        window.show_all()

        self.button = self.builder.get_object("add_row_button")
        self.store = self.builder.get_object("liststore")

        #print all values
        [print(row[:]) for row in self.store]

        self.more_rows = [("four", "", 5739),
                          ("five", "", 120),
                          ("six", "", 4),
                          ("seven", "lucky number", 7),
                          ]

    def main(self):
        Gtk.main()


x = Example()
x.main()

Glade

09_liststore.glade (Source)

<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.20.0 -->
<interface>
  <requires lib="gtk+" version="3.20"/>
  <object class="GtkListStore" id="liststore">
    <columns>
      <!-- column-name name -->
      <column type="gchararray"/>
      <!-- column-name descr -->
      <column type="gchararray"/>
      <!-- column-name num -->
      <column type="gint"/>
    </columns>
    <data>
      <row>
        <col id="0" translatable="yes">one</col>
        <col id="1" translatable="yes">textextext</col>
        <col id="2">12345</col>
      </row>
      <row>
        <col id="0" translatable="yes">two</col>
        <col id="1" translatable="yes">bla blubb</col>
        <col id="2">479</col>
      </row>
      <row>
        <col id="0" translatable="yes">three</col>
        <col id="1" translatable="yes"></col>
        <col id="2">0</col>
      </row>
    </data>
  </object>
  <object class="GtkWindow" id="window">
    <property name="width_request">300</property>
    <property name="can_focus">False</property>
    <signal name="destroy" handler="on_window_destroy" swapped="no"/>
    <child>
      <object class="GtkBox">
        <property name="visible">True</property>
        <property name="can_focus">False</property>
        <property name="orientation">vertical</property>
        <child>
          <object class="GtkComboBox" id="cbox">
            <property name="visible">True</property>
            <property name="can_focus">False</property>
            <property name="model">liststore</property>
            <property name="entry_text_column">0</property>
            <signal name="changed" handler="on_cbox_changed" swapped="no"/>
            <child>
              <object class="GtkCellRendererText"/>
              <attributes>
                <attribute name="text">0</attribute>
              </attributes>
            </child>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="fill">True</property>
            <property name="position">0</property>
          </packing>
        </child>
        <child>
          <object class="GtkBox">
            <property name="width_request">150</property>
            <property name="height_request">250</property>
            <property name="visible">True</property>
            <property name="can_focus">False</property>
            <property name="orientation">vertical</property>
            <child>
              <object class="GtkScrolledWindow">
                <property name="visible">True</property>
                <property name="can_focus">True</property>
                <property name="shadow_type">in</property>
                <child>
                  <object class="GtkTreeView">
                    <property name="visible">True</property>
                    <property name="can_focus">True</property>
                    <property name="model">liststore</property>
                    <property name="headers_clickable">False</property>
                    <child internal-child="selection">
                      <object class="GtkTreeSelection"/>
                    </child>
                    <child>
                      <object class="GtkTreeViewColumn">
                        <property name="title" translatable="yes">Name</property>
                        <property name="sort_indicator">True</property>
                        <property name="sort_column_id">0</property>
                        <child>
                          <object class="GtkCellRendererText"/>
                          <attributes>
                            <attribute name="text">0</attribute>
                          </attributes>
                        </child>
                      </object>
                    </child>
                    <child>
                      <object class="GtkTreeViewColumn">
                        <property name="title" translatable="yes">Description</property>
                        <property name="sort_indicator">True</property>
                        <property name="sort_column_id">2</property>
                        <child>
                          <object class="GtkCellRendererText"/>
                          <attributes>
                            <attribute name="text">1</attribute>
                          </attributes>
                        </child>
                      </object>
                    </child>
                    <child>
                      <object class="GtkTreeViewColumn">
                        <property name="title" translatable="yes">Number</property>
                        <property name="sort_indicator">True</property>
                        <property name="sort_column_id">2</property>
                        <child>
                          <object class="GtkCellRendererText"/>
                          <attributes>
                            <attribute name="text">2</attribute>
                          </attributes>
                        </child>
                      </object>
                    </child>
                  </object>
                </child>
              </object>
              <packing>
                <property name="expand">True</property>
                <property name="fill">True</property>
                <property name="position">0</property>
              </packing>
            </child>
            <child>
              <object class="GtkViewport">
                <property name="visible">True</property>
                <property name="can_focus">False</property>
                <child>
                  <object class="GtkTreeView">
                    <property name="visible">True</property>
                    <property name="can_focus">True</property>
                    <property name="model">liststore</property>
                    <child internal-child="selection">
                      <object class="GtkTreeSelection"/>
                    </child>
                    <child>
                      <object class="GtkTreeViewColumn">
                        <property name="title" translatable="yes">Name</property>
                        <child>
                          <object class="GtkCellRendererText"/>
                          <attributes>
                            <attribute name="text">0</attribute>
                          </attributes>
                        </child>
                      </object>
                    </child>
                    <child>
                      <object class="GtkTreeViewColumn">
                        <property name="title" translatable="yes">Description</property>
                        <child>
                          <object class="GtkCellRendererText" id="cellrenderer_descr">
                            <property name="editable">True</property>
                            <signal name="edited" handler="on_cellrenderer_descr_edited" swapped="no"/>
                          </object>
                          <attributes>
                            <attribute name="text">1</attribute>
                          </attributes>
                        </child>
                      </object>
                    </child>
                    <child>
                      <object class="GtkTreeViewColumn">
                        <property name="title" translatable="yes">Number</property>
                        <child>
                          <object class="GtkCellRendererText"/>
                          <attributes>
                            <attribute name="text">2</attribute>
                          </attributes>
                        </child>
                      </object>
                    </child>
                  </object>
                </child>
              </object>
              <packing>
                <property name="expand">True</property>
                <property name="fill">True</property>
                <property name="position">1</property>
              </packing>
            </child>
          </object>
          <packing>
            <property name="expand">True</property>
            <property name="fill">True</property>
            <property name="position">1</property>
          </packing>
        </child>
        <child>
          <object class="GtkButton" id="add_row_button">
            <property name="label">gtk-add</property>
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="receives_default">True</property>
            <property name="use_stock">True</property>
            <property name="always_show_image">True</property>
            <signal name="clicked" handler="on_add_row_button_clicked" 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">
      <placeholder/>
    </child>
  </object>
</interface>