GTK GUI with PyGObject (9)

Date: 2021/07/22 (initial publish), 2021/07/29 (last update)

Source: en/note-00022.md

Previous Post Top Next Post

TOC

FYI: If the buggy Glade v=3.38.2 is patched, the complicated manual process is not needed.

Client-side decoration with GtkHeaderbar using Glade

Let’s consider to add client-side decorated GtkHeaderBar and to put primary menu with GtkPopover.

This may not be easy with Glade 3.38.2 on upcoming Debian Bullseye 11 since it is buggy. I created a note on my workaround at the end of this page

Let’s assume Glade is fixed by my patch or any other means and let’s use Glade to design GUI.

Now you have:

Glade with CSD titlebar

GtkHeaderBar listed in the left side widget list panel has (titlebar child) added. The GtkHeaderBar widget placed at the top of the GtkWindow widget in the center panel now.

Then I made some more routines such as setting up callback functions for signals.

2 new buttons were used here.

How to create GUI program

Now we are ready to use this touched-up Glade origin GUI design XML file from head_win.py.

import gi

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


@Gtk.Template(filename="head_win.ui")
class Window(Gtk.Window):
    __gtype_name__ = "window"

    entry = Gtk.Template.Child()
    label = Gtk.Template.Child()

    def __init__(self):
        super().__init__()
        self.entry_text = ""

    @Gtk.Template.Callback("on_window_destroy")
    def onDestroy(self, widget):
        Gtk.main_quit()

    @Gtk.Template.Callback("on_button_clicked")
    def onButton(self, widget):
        self.label.set_label("*** " + self.entry_text + " ***")

    @Gtk.Template.Callback("on_entry_activate")
    def onEntry(self, widget):
        self.entry_text = self.entry.get_text()
        self.label.set_label(self.entry.get_text())
        widget.set_text("")

    @Gtk.Template.Callback("on_go_prev_clicked")
    def onGoPrev(self, widget):
        print("Dummy: onGoPrev")

    @Gtk.Template.Callback("on_go_next_clicked")
    def onGoNext(self, widget):
        print("Dummy: onGoNext")

    @Gtk.Template.Callback("on_configure_clicked")
    def onConfigure(self, widget):
        print("Dummy: onConfigure")

    @Gtk.Template.Callback("on_menu1_clicked")
    def onFile(self, widget):
        print("Dummy: onFile")

    @Gtk.Template.Callback("on_menu2_clicked")
    def onEdit(self, widget):
        print("Dummy: onEdit")

    @Gtk.Template.Callback("on_menu3_clicked")
    def onPreference(self, widget):
        print("Dummy: onPreference")

    @Gtk.Template.Callback("on_menu4_clicked")
    def onHelp(self, widget):
        print("Dummy: onHelp")

    @Gtk.Template.Callback("on_menu5_clicked")
    def onAbout(self, widget):
        self.set_title(self.entry_text)


window = Window()
window.set_title("Demo HeaderBar")
window.show_all()
Gtk.main()

Since we use the same name for the assigned variable and the ID in XML, Gtk.Template.Child() takes no argument now.

Since we use different names for callback functions between Python code and UI XML, @Gtk.Template.Callback takes argument such as @Gtk.Template.Callback("on_button_clicked").

Running this with python3 head_win.py will start the GUI as:

Run head_win start

Click on hamburger icon to open primary menu.

Run head_win_click

Please play with this GUI while looking at your terminal outputs.

How touch-upped Glade generated XML looks

For the record, the full XML file head_win.ui is here.

<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.2 -->
<interface>
  <requires lib="gtk+" version="3.24"/>
  <object class="GtkImage" id="image1">
    <property name="visible">True</property>
    <property name="can-focus">False</property>
    <property name="icon-name">edit-undo</property>
  </object>
  <object class="GtkImage" id="image2">
    <property name="visible">True</property>
    <property name="can-focus">False</property>
    <property name="icon-name">edit-redo</property>
  </object>
  <object class="GtkImage" id="image3">
    <property name="visible">True</property>
    <property name="can-focus">False</property>
    <property name="icon-name">gtk-execute</property>
  </object>
  <object class="GtkPopover" id="popover1">
    <property name="can-focus">False</property>
    <child>
      <object class="GtkBox">
        <property name="visible">True</property>
        <property name="can-focus">False</property>
        <property name="orientation">vertical</property>
        <child>
          <object class="GtkModelButton" id="menu1">
            <property name="visible">True</property>
            <property name="can-focus">True</property>
            <property name="receives-default">True</property>
            <property name="text" translatable="yes">File</property>
            <signal name="clicked" handler="on_menu1_clicked" swapped="no"/>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="fill">True</property>
            <property name="position">0</property>
          </packing>
        </child>
        <child>
          <object class="GtkModelButton" id="menu2">
            <property name="visible">True</property>
            <property name="can-focus">True</property>
            <property name="receives-default">True</property>
            <property name="text" translatable="yes">Edit</property>
            <signal name="clicked" handler="on_menu2_clicked" swapped="no"/>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="fill">True</property>
            <property name="position">1</property>
          </packing>
        </child>
        <child>
          <object class="GtkSeparator">
            <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">2</property>
          </packing>
        </child>
        <child>
          <object class="GtkModelButton" id="menu3">
            <property name="visible">True</property>
            <property name="can-focus">True</property>
            <property name="receives-default">True</property>
            <property name="text" translatable="yes">Preferences</property>
            <signal name="clicked" handler="on_menu3_clicked" swapped="no"/>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="fill">True</property>
            <property name="position">3</property>
          </packing>
        </child>
        <child>
          <object class="GtkModelButton" id="menu4">
            <property name="visible">True</property>
            <property name="can-focus">True</property>
            <property name="receives-default">True</property>
            <property name="text" translatable="yes">Help</property>
            <signal name="clicked" handler="on_menu4_clicked" swapped="no"/>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="fill">True</property>
            <property name="position">4</property>
          </packing>
        </child>
        <child>
          <object class="GtkModelButton" id="menu5">
            <property name="visible">True</property>
            <property name="can-focus">True</property>
            <property name="receives-default">True</property>
            <property name="text" translatable="yes">About Application</property>
            <signal name="clicked" handler="on_menu5_clicked" swapped="no"/>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="fill">True</property>
            <property name="position">5</property>
          </packing>
        </child>
      </object>
    </child>
  </object>
  <template class="window" parent="GtkWindow">
    <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="GtkLabel" id="label">
            <property name="visible">True</property>
            <property name="can-focus">False</property>
            <property name="label" translatable="yes">&lt;EMPTY INITIAL&gt;</property>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="fill">True</property>
            <property name="position">0</property>
          </packing>
        </child>
        <child>
          <object class="GtkEntry" id="entry">
            <property name="visible">True</property>
            <property name="can-focus">True</property>
            <signal name="activate" handler="on_entry_activate" swapped="no"/>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="fill">True</property>
            <property name="position">1</property>
          </packing>
        </child>
        <child>
          <object class="GtkButton" id="button">
            <property name="label" translatable="yes">Push this to print entered data</property>
            <property name="visible">True</property>
            <property name="can-focus">True</property>
            <property name="receives-default">True</property>
            <signal name="clicked" handler="on_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">
      <object class="GtkHeaderBar" id="header">
        <property name="visible">True</property>
        <property name="can-focus">False</property>
        <property name="title" translatable="yes">win</property>
        <property name="has-subtitle">False</property>
        <property name="show-close-button">True</property>
        <child>
          <object class="GtkButton" id="go_prev">
            <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_go_prev_clicked" swapped="no"/>
          </object>
        </child>
        <child>
          <object class="GtkButton" id="go_next">
            <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_go_next_clicked" swapped="no"/>
          </object>
          <packing>
            <property name="position">1</property>
          </packing>
        </child>
        <child>
          <object class="GtkMenuButton" id="menu">
            <property name="visible">True</property>
            <property name="can-focus">True</property>
            <property name="focus-on-click">False</property>
            <property name="receives-default">True</property>
            <property name="popover">popover1</property>
            <child>
              <object class="GtkImage">
                <property name="visible">True</property>
                <property name="can-focus">False</property>
                <property name="icon-name">open-menu-symbolic</property>
              </object>
            </child>
          </object>
          <packing>
            <property name="pack-type">end</property>
            <property name="position">2</property>
          </packing>
        </child>
        <child>
          <object class="GtkButton" id="configure">
            <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="clicked" handler="on_configure_clicked" swapped="no"/>
          </object>
          <packing>
            <property name="pack-type">end</property>
            <property name="position">4</property>
          </packing>
        </child>
      </object>
    </child>
  </template>
</interface>

Workaround of Glade bug

This is a note of workaround for Glade 3.38.2 which may be useful for other situation too.

If you already put something into GtkWindow, the above fails. Very similar bug exists for GtkPOpoverMenu.

To work around this bug, the GtkHeadBar can be placed anywhere on Glade drawing area but placing it at the end of normal window widgets makes touching up its UI XML file with the text editor easy for me later.

(Although it looked easier to reorder widgets via the text editor cut-and-paste initially, it is actually easier to play with “Pack type” and Position" in Packing tab of involved widgets.)

Glade initial GUI design

Activating the “Client-side window decorations” checkbox creates odd non-editable blank area inside the window. Trying to put a widget there causes the error message as follows:

Glade not woring as expected

This seems to be intentional design of Glade to deal newly introduced XML structure support with minimal efforts on time.

Please look into the UI XML file after above change. There is an odd XML tag <placeholder/> which has been added inside of the newly added <child type="titlebar">...</child>-tag pair. This meaning of type="titlebar" is found in GtkWindow as:

The Gtk.Window implementation of the Gtk.Buildable interface supports setting a child as the titlebar by specifying “titlebar” as the “type” attribute of a <child> element.

<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.2 -->
<interface>
  ...
  <template class="window" parent="GtkWindow">
    <property name="can-focus">False</property>
    <child>
      ...
        <child>
          <object class="GtkHeaderBar" id="header">
            ... (snipped)
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="fill">True</property>
            <property name="position">4</property>
          </packing>
        </child>
      </object>
    </child>
    <child type="titlebar">
      <placeholder/>
    </child>
  </template>
</interface>

This <placeholder/> is sloppy but practical Glade support of “Client-side window decorations”.

Since Glade seems to expect us to manually touch-up this XML file, let’s do it.

First move <child><object class="GtkHeaderBar" id="header">...</child> to just after <placeholder/> and remove <placeholder/>.

Then remove now redundant <child> and </child> tags from newly moved section. Indentation is non-significant. Glade also functions as a pretty printing tool.

<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.2 -->
<interface>
  ...
  <template class="window" parent="GtkWindow">
    <property name="can-focus">False</property>
    <child>
      ...
    <child type="titlebar">
      <object class="GtkHeaderBar" id="header">
        ... (snipped)
      </object>
      <packing>
        <property name="expand">False</property>
        <property name="fill">True</property>
        <property name="position">4</property>
      </packing>
    </child>
  </template>
</interface>

If you open this manually modified XML file, you should get the desired result.

I verified the same trick works for the GtkPopoverMenu bug, too.

You may read followings:

Previous Post Top Next Post