Updating Warble to GTK4

How I updated Warble to GTK4, and a few of the issues I encountered

Warble is officially at version 2.0.0, which means it’s now using GTK4!

It was a fun challenge to update Warble from GTK3 to GTK4, although a bit frustrating at times. I hope that by walking through some of the specific changes I made, you might not struggle quite as much as I did! Wherever possible I will include links to the diff in GitHub so you can see the exact change that I made.

You can view the full diff of changes here: avojak/warble/pull/39

Updating the Dependencies

The obvious first step is to, well, pull in GTK4 as a dependency!

1
dependency('gtk4', version: '>= 4.6.3')

And if you’re using an IDE, you’ll immediately be met with a giant slap in the face. Compile errors out the wazoo! If you’re like me, you had no idea that Libhandy was going away (and I was just getting used to it…). Turns out, Libadwaita is the successor. Not to worry, this is an easy substitution!

1
2
3
dependency('libadwaita-1', version: '>= 1.1.0'),
# I'm using Granite as well, so you need to grab the new version of it as well!
dependency('granite-7', version: '>= 7.0.0')

Now all you have to do is quickly rush through your code, take a quick guess at how to fix the compile errors, rebuild, run, and…

Welp. This was when I realized that it wasn’t going to be a quick update!

Helpful Resources

I’m going to pause briefly here to list some resources that I found helpful along the way:

  • GTK - Migrating 3 to 4 - This guide was very helpful at identifying specific API changes, and the replacements. When in doubt, Ctrl+f on this page first!
  • GTK Inspector - The GTK inspector application is incredibly useful in general, but specifically when you’re making UI changes. If you’re using Flatpak, simply add --env=GTK_DEBUG=interactive to your flatpak run command and the Inspector will open alongside your app!
  • Valadoc.org - Another probably obvious resource, but nothing beats reading the docs.

Input Handling

There are two main input methods in Warble: using the physical keyboard attached to the computer, and the on-screen keyboard.

Keyboard Input

With GTK3 and Libhandy, I used the key_press_event signal on the Hdy.Window, but GTK4 introduced a new event controller system.

1
2
3
var key_event_controller = new Gtk.EventControllerKey ();
key_event_controller.key_pressed.connect (on_key_pressed_event);
((Gtk.Widget) this).add_controller (key_event_controller);

A relatively small difference in this case, but worth noting.

Mouse Input

Mouse input is more significantly different. Instead of needing to use widgets such as EventBox, and using Gdk.EventMask for the various event types you care about, we now use Gtk.GestureClick (or other similar controllers depending on the desired event type):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var gesture_event_controller = new Gtk.GestureClick ();
gesture_event_controller.pressed.connect ((n_press, x, y) => {
    if (n_press != 1) {
        return;
    }
    get_style_context ().add_class ("key-pressed");
    clicked (letter);
});
gesture_event_controller.released.connect ((n_press, x, y) => {
    if (n_press != 1) {
        return;
    }
    get_style_context ().remove_class ("key-pressed");
});
this.add_controller (gesture_event_controller);

(Diff)

Final Classes

One error you will most certainly encounter is field 'parent_instance' has incomplete type. This is because many Gtk classes are now final and cannot be subclassed. I ran into this because I had subclassed Gtk.HeaderBar. In my case I simply created the header bar directly in a different class, but in other cases I had to use a different container widget.

Drawing with Cairo

Under GTK3, the method for doing custom drawing on a widget was to override the draw(Cairo.Context) method:

1
2
3
4
5
6
7
protected override bool draw (Cairo.Context ctx) {
    base.draw (ctx); // Draw the widget as normal
    ctx.save ();
    custom_drawing (ctx); // Do some custom drawing after the base widget is drawn
    ctx.restore ();
    return false;
}

For GTK4, there’s a different approach that I can use. To draw the squares on the game board with letters, the squares are now Gtk.DrawingArea widgets. This class exposes a set_draw_func(Gtk.DrawingArea da, Cairo.Context ctx, int width, int height) method with some new convenience parameters for width and height.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void draw_func (Gtk.DrawingArea drawing_area, Cairo.Context ctx, int width, int height) {
    var color = Gdk.RGBA ();
    color.parse (Warble.ColorPalette.get_text_color (state));
    ctx.set_source_rgb (color.red, color.green, color.blue);

    ctx.select_font_face ("Inter", Cairo.FontSlant.NORMAL, Cairo.FontWeight.BOLD);
    ctx.set_font_size (FONT_SIZE);

    Cairo.TextExtents extents;
    ctx.text_extents (letter.to_string (), out extents);
    double x = (width / 2) - (extents.width / 2 + extents.x_bearing);
    double y = (height / 2) - (extents.height / 2 + extents.y_bearing);
    ctx.move_to (x, y);
    ctx.show_text (letter.to_string ());
}

(Diff)

Window Positioning

Gone are the days of saving the position of the application window to the GSettings - now the window manager handles that for you!

(Diff)

CSS Blur

This isn’t a change, but rather a new feature in GTK4 that I love. You can now use a blur() filter. In Warble, I combine blur with transparency on certain overlay views as a nice replacement for using a dialog:

1
2
3
4
5
6
.faded {
    transition-property: opacity, filter;
    transition-duration: 0.5s;
    filter: blur(5px);
    opacity: 0.3;
}

I’m a big fan of how this turned out:

Warble screenshot with blur

Granite ModeButton No More

Not necessarily GTK4 related, but the ModeButton widget was removed from Granite. This is really a “shame on me” moment for using this widget incorrectly, but it might pose a “gotcha” to anyone else using it in a similar way.

The replacement is to use grouped Gtk.CheckButton widgets. For Warble, I created a simple wrapper widget (CheckButtonGroup) to handle the grouping.

I think the GTK method of grouping widgets in this case is rather clumsy. One common theme in GTK4 is the use of proper container widgets, yet here we create a “group” by assigning a sibling widget to the group property. Huh?? Now you end up with a widget that’s pulling double-duty as both an individual button, but also the group. In my opinion, a better design would be to create a specific group container widget that you add buttons to.

CSS Changes

While not directly related to GTK4, I took this opportunity to leverage CSS for styling the game area and on-screen keyboard instead of using custom icons for each possible color scenario.

The icons for the squares were replaced with two lines of code to apply some CSS classes:

1
2
get_style_context ().add_class (Granite.STYLE_CLASS_CARD);
get_style_context ().add_class (Granite.STYLE_CLASS_ROUNDED);

And the various states for the squares and keyboard keys were also represented with CSS classes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.guess-correct {
    background-color: @LIME_300;
    border: 1px solid @LIME_700;
}

.guess-incorrect {
    background-color: @SILVER_500;
    border: 1px solid @SILVER_900;
}

.guess-close {
    background-color: @BANANA_300;
    border: 1px solid @BANANA_700;
}

.key-pressed {
    transform: translateY(2px);
}

.tile-active {
    border: 1px solid @BLACK_700;
}

When switching to high-contrast mode, all I had to do was swap out the stylesheet - no more changing ALL the icons!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void load_theme_stylesheet () {
    var gtk_settings = Gtk.Settings.get_default ();
    bool dark_mode = gtk_settings.gtk_application_prefer_dark_theme;
    bool high_contrast_mode = settings.get_boolean ("high-contrast-mode");

    if (dark_mode && high_contrast_mode) {
        theme_provider.load_from_resource (Constants.APP_ID.replace (".", "/") + "/warble-dark-hicontrast.css");
    } else if (dark_mode && !high_contrast_mode) {
        theme_provider.load_from_resource (Constants.APP_ID.replace (".", "/") + "/warble-dark.css");
    } else if (!dark_mode && high_contrast_mode) {
        theme_provider.load_from_resource (Constants.APP_ID.replace (".", "/") + "/warble-light-hicontrast.css");
    } else {
        theme_provider.load_from_resource (Constants.APP_ID.replace (".", "/") + "/warble-light.css");
    }
}

As an added bonus, I was able to very easily add a subtle outline for the square on the game board that’s currently waiting for input. Previously this would’ve required a whole new icon.

End Result

Check out the end result over on GitHub!

Some rights reserved

Up Next

Installing ESXi with a USB NIC

Creating a custom ISO for ESXi 7 with a USB NIC

Grafana Dashboard for Pi-hole Stats

Excessive Pi-hole monitoring with pretty graphs!

Installing VMware ESXi on a Raspberry Pi

How to install VMware ESXi 7 on a Raspberry Pi 4

Related