GSoC part 12: the finishing touches

Posted:  • 5 minute read • Last modified:

GSoC logo horizontal

Last week I shared the news that all large features had been implemented; all that was left were issues raised by GNOME contributors when my mentor demoed my progress over GUADEC. This week I’ve just been chugging away on those issues; let me show you.

Obvious save button is.. not obvious?

Hadess noted that the save button wasn’t obvious when my mentor demoed Piper to him; I already mentioned this last week: the icon is a save to disk icon and it’s not obvious at first glance that you have to press it to write the changes you made to the device. I discussed that we want to go for a time-based commit, where the changes are committed to the device automatically after certain events (e.g. switching stack pages) or after a certain interval of inactivity. Since that will require more work and is thus something for a later version, I went with the other options to at least solve the problem for now:

As you can see, the icon has been replaced with text and the button is now insensitive when there are no changes to be committed. When there are, the button is sensitive and the suggested-action CSS class is applied to the button, in order to draw the user’s attention that something needs to be committed. You can view the pull request here.

Shutdown confirmation

Related to the above (and unsurprisingly also suggested by hadess) is asking for confirmation when the user attempts to close Piper with unsaved changes:

This shows the versatility of the perspective abstraction; all it takes is adding a single new property to each perspective:

def can_shutdown(self):
    """Whether this perspective can safely shutdown."""
    for profile in self._device.profiles:
        if profile.dirty:
            return False
    return True

This property signals whether a perspective can safely shutdown. On a delete event, the window checks all its perspectives' properties and if one perspective signals that it cannot safely shutdown, it presents the dialog:

def do_delete_event(self, event):
    for perspective in self.stack_perspectives.get_children():
        if not perspective.can_shutdown:
            dialog = Gtk.MessageDialog(self, Gtk.DialogFlags.MODAL,
                                       _("There are unapplied changes. Are you sure you want to quit?"))
            response =

            if response == Gtk.ResponseType.NO or response == Gtk.ResponseType.DELETE_EVENT:
                return Gdk.EVENT_STOP
    return Gdk.EVENT_PROPAGATE

Device (dis)connects

This was the last unimplemented feature that my mentor and I agreed Piper had to have when we came up with the GSoC proposal. I saved it for last because it’s quite a niche case to own (let alone use simultaneously) two devices, but it had to be done at some point. The welcome perspective was already implemented last week; it was simply a matter of adding the right signals to the ratbagd bindings, fixing said bindings, adding methods to add and remove devices from the welcome perspective and connecting the dots. For the last step, it was a bit of puzzling to figure out all the scenarios but I believe I’ve got them all:

When a device is added and there:

When a device is removed and:

Of course you can mix and match these, for example if you are configuring a device, disconnect it and then connect it again you’ll jump straight back into editing it.

Back button

When multiple devices are connected, you might want to configure more than one as well. It is inconvenient to have to close and open Piper again, so for this scenario I have given perspectives the option to declare whether they want a back button to be shown, which will take the user back to the welcome perspective to switch between devices:

Again we add another property to the perspectives interface:

def can_go_back(self):
    """Whether this perspective wants a back button to be displayed in case
    there is more than one connected device."""
    return True

This allows the window class to insert a back button into the perspectives' titlebars and prevents all perspectives of having to add it themselves:

def _add_perspective(self, perspective, ratbag):
    if perspective.can_go_back:
        button_back = Gtk.Button.new_from_icon_name("go-previous-symbolic",
        button_back.set_visible(len(ratbag.devices) > 1)
        button_back.connect("clicked", lambda button, ratbag:
        ratbag.connect("notify::devices", lambda ratbag, pspec:
                       button_back.set_visible(len(ratbag.devices) > 1))
        # Place the button first in the titlebar.
        perspective.titlebar.child_set_property(button_back, "position", 0)

That’s a small amount of code for another large dose o' polish!

Other smaller changes

In making ratbagd use enums everywhere as opposed to strings for some properties, the ResolutionsPage was left behind and couldn’t display which buttons were assigned special mappings that relate to resolutions. I fixed that this week; you can see it work again in the videos above.

Last week I dropped indices from the UI, but this left the resolution rows looking quite empty. The solution is to simply make them less wide, although this might change when we cannot fix the range of the resolution scale logarithmically or by simply limitting it, as discussed in the linked pull request and this issue.

The last noteworthy change of this week is that the key capture for macros now also supports key releases, but this isn’t merged yet. We decided to drop button capture for now, as this isn’t (yet?) properly supported by libratbag.

That’s it for this week! The coming week will be more of the same: adding spit ‘n polish where it is needed the most.

This blog post is part of a series. You can read the next part about saving the planet here or the previous part about the welcome screen here.