pcb-rnd knowledge pool


GTK4 rant: menu for a single use case

gtk4r_menu by Tibor 'Igor2' Palinkas on 2021-12-23

Tags: insight, gtk4, rant, menu, hotkey, accel

node source



Abstract: n/a

  This rant is part of a series.

Menu and accel

In a nice library API there are distinct functionalities or sub-APIs doing separate things. If the application wants, it can combine, connect those different APIs but it's easily possible to use one API without the other. In an ideal world the main menu of a GUI application and how hotkeys are handled could be like that.

A main menu system is really just a list of lists of choices presented recursively in popup windows. The application would create those lists in memory and the GUI would present them when the user clicked the menu bar or an open menu that has submenus. It's really a tree, with non-leaf menu items opening new submenus and actions executed at the leaf nodes.

A hotkey, or "accel" for "keyboard accelerator" in gtk's terminology, is that the user may press a key, or a combination or sequence of keys to invoke an action without having to click for the menus. The user may learn the accel key for his most commonly used actions because pressing keys are usually much faster than opening a stack of submenus.

These two systems are seemingly independent, except for two things:

Still, they could be two separate APIs, each replaceable with different implementations.

For example in a feature rich CAD system like pcb-rnd, your main menu is really a huge tree of submenus, while on a smaller application with less functionality it may be a single, flat list of a handful of actions. You may want to have a totally different GUI representation of the two, to keep the complex app efficient while keep the small app simple.

The same way, in a small app hotkeys are like ctrl+s or alt+q. In a large app this stops working after a while and you really need to use key sequences for hotkeys, such as {f s}, which means "press f, release, then press s", or in short "type f s". Typing a 2..3 stroke sequence of plain lowercase letters is typically faster than the acrobatics needed for an alt+shift+s. Plus it allows developers to assign easier to remember hotkeys because the tree of choices is not limited by 3 modifier keys. So it is reasonable to use two different implementations, a simple, single-stroke modifier based one for small applications and a sequence based one for complex apps.

But gtk2...

But in gtk that was never the case. In gtk2 "accel" was already tied to the menu system: because of the assumption that hotkeys are doing the same action as menus, the two systems got tangled in many different ways. It was very hard to use one without the other. And the "accel" system offered only single-stroke keys.

When pcb-rnd moved from single-stroke hotkeys to sequences, I decided to implement a single, central hotkey handler so all the different GUI HIDs only need to capture key press events and pcb-rnd will work the same on all. In case of gtk2, without having to work around the "accel" code limitation of single-stroke.

But then came the problem: stock menu widgets can't print an arbitrary text label for hotkeys, because the menu system is tied up with the accel system. The easiest solution was to replace the menu item widget with our own that doesn't try to hook up with the accel system, just blindly display menu text and an arbitrary hotkey label.

In other words: gtk2's model is that the menu implementation is tied together with the accel implementation, everything stored in the menu model. It provides the model of how your application needs to work: single-stroke hotkeys tied to menu items. This makes it easy to build menus and use hotkeys for a simple application but makes the system unusable if you want any little detail differently. Since the model is part of the menu system and is part of gtk, and all participants (menu items and accel key system) try to be smart and connect together in the background, you can't easily change one part.

Meanwhile pcb-rnd's new model was: have a central, app-specific model of menus and hotkeys then menu items and hotkey handling should be simple, stupid, slave systems. A menu system should just blindly display items calculated by the model, not knowing why the hotkey string is what it is and hotkey processing should not care about how the hotkey is displayed in the menu, just execute the action. For this the easiest way was to lie to gtk and pretend we don't have accels at all, then replace the menu item widget with one that can display an arbitrary string for hotkey then act on keyboard events directly.

Gtk4 made it worse

Knowing all that I started the gtk4 port of the menu system by looking at how I can implement the same lie and how I could replace the menu item widget with my own that doesn't care about other subsystems just displays the hotkey text it was given. Then I figured gtk4 complicated gtk2's system a bit more: it introduced a new menu model concept on glib level. Basically it moved out the menu model from the menu widget implementation, which would normally be a good idea. But now there are three gtk4/glib things interconnected in various ways:

And of course all have tons of assumptions, including single-stroke hotkeys and that you don't want tear-off menus because these things are not very useful in simple apps.

I looked at how complicated it would be to load the glib menu model from librnd's menu model and to implement a custom menu widget that is compatible with glib's menu model, but it all looked too complicated. Basically one more level of abstraction I never needed, with bondage&discipline design.


Since I already have a HID-independent, central menu model and a HID-independent central hotkey model, I decided to implement the whole main menu system from scratch. Since I needed multi-stroke hotkeys and tear-off menus, I believe it was cheaper than trying to figure how to extend the existing gtk4 menu implementation that already had 2 layers more than I needed.

The menu implementation in gtk2, including the custom "dumb display" menu widget and the code that loads librnd's menu model into gtk2 weights about 420 sloc. Our gtk4 implementation, that needs to implement the "dumb display" menu widget, populate the model and also implement the whole GUI menu machinery and tear-off menus (missing from gtk4) is 615 sloc. Which means reimplementing the existing menu mechanism costed less than 200 sloc.

An interesting aspect of this is how much the code depends on gtk4 APIs. With the custom menu implementation, we use only the "tip of the iceberg": we open popup windows and place list-of-widgets in them, things we do elsewhere for non-menu things already. If I wanted to implement the "elegant" approach, the resulting code would need to use a lot of glib menu model API, gtk4 menu API, things that we don't use elsewhere, APIs a bit deeper. If gtk4 APIs change over the coming years, our current "reimplement from scratch" approach is much more stable as it uses much less in volume and much less in depth APIs.