When I proposed the project with my mentor, we worked out a bunch of features that the new Piper should have. These are listed in the Redesign Wiki, but here’s a high-level summary:
- A welcome screen, presenting a list of connected and supported devices.
- An error screen, presenting any problems in a user-friendly manner.
- The main screen, presenting the configuration pages:
- Support for device profiles (see part 9 for the start on this).
This week, I finished the remaining items! You would think I’m done with Google Summer of Code now, but alas, my mentor opened quite a few issues from demoing Piper to a bunch of awesome people while at GUADEC! That’s oke though, I wouldn’t know what else to do with my summer anyway… 😉 Before I get lost in dreaming about the summer that could have been, let’s get to the point!
Really finishing the button page
Last week I said that the button page was done and pending a review before being
merged. While it was pending for review, it turned out it wasn’t really ready to
be merged just yet: due to cleaning up the history and rebasing the work on the
other changes such as the DBus interface rewrite, some things got left behind
resulting in a not-really-functional button dialog. Fortunately it wasn’t all
entirely my fault; while debugging the
RatbagdButton doesn’t emit
PropertyChanged signals for its
ActionType property, which means that Piper doesn’t get notified when this
property changes. The fix was
rather straightforward and together with an
to the bindings resolved most of the issues. Finally, the button page was
really done (yes, really!) and it is now merged!
Last week I also added the widgets to Piper to support profiles. The second, more difficult part was to come up with an architecture that threaded a profile change in the least intrusive manner. Supporting profiles means that all widgets
- update their state to reflect new values in the new profile. For example, a resolution scale displaying resolution 0’s DPI of 50 in profile 0 should, when the profile changes to 1, display resolution 0’s DPI of 100 in profile 1 (if resolution 0 in profile 1 has a resolution of 100 of course, but bear with me here).
- apply their changes to the correct resolution. Using the same example, the scale should now change the DPI of resolution 0 in profile 1, and not resolution 0 in profile 0.
For this to work, each widget needs to be aware of profile changes so that it can retrieve the new profile, through which it can retrieve the setting it controls in order to perform items 1 and 2 above. For example, a resolution scale controlling resolution 0 for profile 0, should now retrieve resolution 0 from profile 1 and control that. I envisioned three ways to implement this:
- Pass the list of all profiles to each object that needs to react to profile
changes. These objects then connect to each profile’s
notify::is-activesignal. In this callback they then retrieve the new active profile and update themselves as they please. I didn’t like this approach because it requires passing the full list of resolutions to each object.
- Emit a
profile-changedsignal on the device. This way, an object can connect to the
RatbagdDevice::profile-changedsignal, where the new active profile is passed to the callback. This approach is better because it doesn’t require passing the list of all profiles to each object, but only the device. Many objects already need the device anyway, so this doesn’t add much overhead.
- Have each object that needs to react to profile changes implement a certain
on_profile_changed), where this method is called recursively by each parent on its children (e.g.
ResolutionsPagecalls it on each
ResolutionRow), starting from
Window. I didn’t like this because it would require some kind of (abstract) base class or interface to ensure that each class implements this method, a concept which is quite alien to Python and thus hard to enforce.
I proposed all approaches to my mentor, who agreed that number two was the best one. After this was worked out, implementing profiles wasn’t much work at all, confirming that we chose the right approach. You can check the commits for the individual widgets to see how easy this was; but all in all I estimate that the actual threading and updating the widgets is less than a hundred lines of code.
From the much too abstract list of features in the beginning of this post, the only items that aren’t linked to are the welcome and error screens. No, I didn’t forget about those; I just saved those for last. To me, it was the least essential feature as owning more than one device, let alone using them simultaneously, is a niche case. As it turns out, however, the changes I made while implementing these screens also pave the way for eventual keyboard support. Let me explain!
The welcome and error screen both provide a different
view into the same
application window. If we add the configuration screen, that gives three such
different views. To allow for these different views, I added the concept of a
perspective, which I define as a certain view into Piper.
A perspective needs to implement an interface of sorts (something something Python) with two methods: one to retrieve the name of the perspective, and another to retrieve its titlebar widget. Different scenarios can then fully control Piper’s window by providing their own main- and titlebar widget.
Adding the perspectives is simply a
matter of the iterating through them in
def __init__(self, ratbag, *args, **kwargs): Gtk.ApplicationWindow.__init__(self, *args, **kwargs) self.init_template() perspectives = [ErrorPerspective(), MousePerspective(), WelcomePerspective()] for perspective in perspectives: self._add_perspective(perspective) def _add_perspective(self, perspective): self.stack_perspectives.add_named(perspective, perspective.name) self.stack_titlebar.add_named(perspective.titlebar, perspective.name)
Setting a perspective is a matter of finding the widget from the
making it active. For example, for the
def _present_error_perspective(self, message, detail): error_perspective = self.stack_perspectives.get_child_by_name("error_perspective") error_perspective.set_message(message) error_perspective.set_detail(detail) self.stack_titlebar.set_visible_child_name(error_perspective.name) self.stack_perspectives.set_visible_child_name(error_perspective.name)
The only part I don’t like here is that the
Window needs to know the
perspective names to find them in the stack, but other than that this makes for
a quite clean implementation, fully adhering to the single responsibility
principle. You can check this for yourself in the pull
As I mentioned last week, my mentor was at GUADEC together with bentiss. While there, he demoed Piper to GNOME designers and other contributors. I’m pretty happy with the result, as the issues pointed out are all relatively minor; I think the lack of major issues is a sign we’re doing something right ☺
Firstly one issue that I fixed already is that users generally aren’t interested
in indices, especially not those
that start at zero (
normal people start counting at one – Jakub
Secondly, the largest issue is probably that the save button is not
obvious: the icon is a
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. For the former, it’d help to replace
the image with text, such as
Write to Device, while for
the latter we might want to highlight or flash the button whenever a profile is
dirty. While I like the latter, I’m not a fan of replacing the image with text.
Luckily, bentiss suggested that we should do away with explicit saving and
implement a time-based auto-commit feature instead. The issues of
discoverability and aborting changes that I raised were dismissed with that the
dialogs have cancel buttons and that the tools on other operating systems also
use timeouts. Let’s see how this will work out.
Thirdly, it wasn’t obvious that the resolution scales have increments that snap. Due to the rather large DPI range of 50 to 12000 (inclusive), the increments of 50 are so small that the snapping is barely noticeable. We’re discussing right now whether a DPI of 12000 is even realistic; if we limit the DPI to a reasonable range of say 50 - 6000 and use steps of 100 things should already be a little better. I suppose this will simply be a case of trying different ranges and step sizes, and then seeing if people complain about the limited range.
Furthermore, an interesting issue was raised: instead of having leader lines
next to the device SVG (see parts 4 and
6), the keys themselves in the SVG could be clickable. This
should be doable, but Jakub Steiner, my mentor and I also discussed a
capture mode where Piper grabs the mouse and listens for key presses to
open that key’s configuration. This is an open question still, but definitely
interesting to think about!
Furthermore, libratbag needs to return sane defaults for settings that are currently not enabled, the resolution scale can be hidden at the end of the list, the button map dialog needs a search field because the list is too long, profile cycle buttons need to apply across all profiles to prevent landing in a profile with no way out when using said button, the active resolution needs to be highlighted in the list, changing a resolution should make that resolution the active one so that the user can see the effects automatically, Piper shouldn’t allow unsetting the left mouse button because we don’t want the user to end up without one, keyboard support should be improved, Piper should allow the user to set profile names and Piper should handle device disconnects and newly connected devices.
As you can see, there’s plenty left to do the coming weeks! As my British classmate says, onwards and upwards!