Styling GTK3 in Python with CSS (Shallow Thoughts)

Akkana's Musings on Open Source Computing and Technology, Science, and Nature.

Wed, 05 Jun 2019

Styling GTK3 in Python with CSS

Lately I've been running with my default python set to Python 3. Debian still uses Python 2 as the default, which is reasonable, but adding a ~/bin/python symlink to /usr/bin/python3 helps me preview scripts that might become a problem once Debian does switch. I thought I had converted most of my Python scripts to Python 3 already, but this link is catching some I didn't convert.

Python has a nice script called 2to3 that can convert the bulk of most scripts with little fanfare. The biggest hassles that 2to3 can't handle are network related (urllib and urllib2) and, the big one, user interfaces. PyGTK, based on GTK2 has no Python 3 equivalent; in Python 3, the only option is to use GObject Introspection (gi) and GTK3. Since there's almost no documentation on python-gi and gtk3, converting a GTK script always involves a lot of fumbling and guesswork.

A few days ago I tried to play an MP3 in my little musicplayer.py script and discovered I'd never updated it. I have enough gi/GTK3 scripts by now that I thought something with such a simple user interface would be easy. Shows how much I know about GTK3!

I got the basic window ported pretty easily, but it looked terrible: huge margins everywhere, and no styling on the text, like the bold, large-sized text I had previously use to highlight the name of the currently playing song. I tried various approaches, but a lot of the old methods of styling have been deprecated in GTK3; you're supposed to use CSS. Except, of course, there's no documentation on it, and it turns out the CSS accepted by GTK3 is a tiny subset of the CSS you can use in HTML pages, but what the subset is doesn't seem to be documented anywhere.

How to Apply a Stylesheet

The first task was to get any CSS at all working. The GNOME Journal: Styling GTK with CSS was helpful in getting started, but had a lot of information that doesn't work (perhaps it did once). At least it gave me this basic snippet:

    css = '* { background-color: #f00; }'
    css_provider = gtk.CssProvider()
    css_provider.load_from_data(css)
    context = gtk.StyleContext()
    screen = Gdk.Screen.get_default()
    context.add_provider_for_screen(screen, css_provider,
                                    gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)

Built-in Class Names

Great! if all you want to do is turn the whole app red. But in reality, you'll want to style different widgets differently. At least some classes have class names:

    css = 'button { background-color: #f00; }'
I found other pages suggesting using 'GtkButton in CSS, but that didn't work for me. How do you find the right class names? No idea, I never found a reference for that. Just guess, I guess.

User-set Class Names

What about classes -- for instance, make all the buttons in a ButtonBox white? You can add classes this way:

    button_context = button.get_style_context()
    button_context.add_class("whitebutton")

If you need to change a class (for instance, turn a red button green), first remove the old class:

    button_context = button.get_style_context()
    entry_style_context.remove_class("red")

Widget Names, like CSS ID

For single widgets, you can give the widget a name and use it like an ID in CSS. Like this:

    label = gtk.Label()
    label.set_use_markup(True)
    label.set_line_wrap(True)
    label.set_name("red_label")
    mainbox.pack_start(label, False, False, 0)
    css = '#red_label { background-color: #f00; }'
[ ... ]

Properties You Can Set

There is, amazingly, a page on which CSS properties GTK3 supports. That page doesn't mention it, but some properties like :hover are also supported. So you can write CSS tweaks like

.button { border-radius: 15; border-width: 2; border-style: outset; }
.button:hover { background: #dff; border-color: #8bb; }

And descendants work, so you can say somthing like

    buttonbox = gtk.ButtonBox(spacing=4)
    buttonbox.set_name("buttonbox")
    mainbox.pack_end(buttonbox, False, False, 0)

    btn = gtk.Button(label="A")
    buttonbox.add(btn)

    btn = gtk.Button(label="B")
    buttonbox.add(btn)
and then use CSS that affects all the buttons inside the buttonbox:
#buttonbox button { color: red; }

No mixed CSS Inside Labels

My biggest disappointment was that I couldn't mix styles inside a label. You can't do something like

label.set_label('Headline'
                'Normal text')

and expect to style the different parts separately. You can use very simple markup like <b>bold</b> normal, but anything further gives errors like "error parsing markup: Attribute 'class' is not allowed on the <span> tag" (you'll get the same error if you try "id"). I had to make separate GtkLabels for each text size and style I wanted, which is a lot more work. If you wanted to mix styles and have them reflow as the content length changed, I don't know how (or if) you could do it.

Fortunately, I don't strictly need that for this little app. So for now, I'm happy to have gotten this much working.

Tags: , , ,
[ 14:49 Jun 05, 2019    More programming | permalink to this entry | comments ]