Welcome to my personal web page
Some time ago I mentioned working on the global application menu for Qt5 - the so-called appmenu-qt5 for Ubuntu and its derivatives. After a longer while, I finally fount the time and occasion to resume my work - and, after a really bumpy ride, end up with a working solution. Some hacks had to be made, some Qt5 design decisions worked-around - but the end result is here: a working appmenu-qt5 QPA platformtheme plugin. In this post I would like to overview the implementation of the current proposed appmenu-qt5. Read on if you're interested in some of the Qt5 internals, workings of QPlatformTheme plugins and the confusing elements of the Qt Platform Abstraction in overall.
First of all I would like to mention is the lack of any solid documentation regarding QPA and the platform theming features in Qt5. The most useful 'source' of knowledge was, well, the source-code itself.
How does the current implementation work? It's a bit shameful, but I actually worked around the whole core design of platform menu-bars that the Qt5 developers invented. Last time I overviewed how in our case we would have to duplicate menu information by providing QPlatformMenu* equivalents for every QMenu* element. As mentioned, I ditched that, instead listening only for the QPlatformMenuBar creation to try and export the related QMenuBar to DBus through DBusMenuExporter. The problem is: when we get the createPlatformMenuBar() call from QPA, we get NO information on what QMenuBar we want to export! A bit too abstract for my taste. So, what I do is 'hack around' and fetch the QWidget that is the parent of the menu bar needing export.
So, in the end, after getting the QMenuBar of the application currently running, I am doing a cast to QMenu and pushing this very object to export through DBus. This way DBusMenuExporter itself handles all the cases of menus/items being added and removed without any additional code-paths.
The most tricky part was the QWidget acquisition, which I briefly mentioned before already. The handleReparent() method only accepts a QWindow argument. The QWindow -> QWidget translation is not directly documented anyway. By browsing through the available methods, I noticed that we can use the following trick, visible below. It's not protecting us against possible switching of the QMenuBar in a given QWidget, but that case happens really rarely. It's still something that needs to be remembered for the future though.
# How to fetch a QWidget paired with a given QWindow # window is the pointer to a QWindow QWidget *widget = QWidget::find(window->winId());
The other interesting part worth mentioning is actually how to write QPlatformTheme plugins and how those are loaded. Let's have a separate session for it:
The final decision I made was to use the QPlatformThemePlugin approach. Qt5 offers including platform theme plugins in the platformplugin/ plugin directory. Those are looked for and loaded first by QGuiApplication. The catch is, though, that Qt5 does not look into the directory and try all the available theme plugins there. It tries to fetch the theme plugins basing on what the themeNames() method of the currently selected QPlatformIntegration returns. I started wondering if there is a way of overriding which theme names are returned.
Sadly, in the current Qt5 implementation - there is no way. For the xcb QPA plugin which is used by default in Ubuntu right now, the QGenericUnixTheme::themeNames() fetches the DESKTOP_SESSION environment variable and adds to the theme names, but it adds it to the end of the list. Since theme plugins are being checked one-by-one from first to last, in a normally working system, it's impossible to get our plugin running. For testing, I distro-patched my Qt5 to allow overriding the QGenericUnixTheme's theme name through the DESKTOP_SESSION variable (patch available here), but I'm still looking for a better solution.
The other quirk is - when using QPlatformThemePlugin and QPA in overall, it's not possible to just say: "override only the platform menu, use everything else that you were using up until now". The effect of this is: appmenu-qt5 has to detect if we're running something Gnome-theme like or KDE-theme like and then theme itself accordingly. It's basically deciding whether to use QGnomeTheme or QKdeTheme as the base class (instead of the clean, themeless QPlatformTheme). Those two classes are part of QGenericUnixThemes for the most popular GNU/Linux themes, and without those used we would get ugly X-themed windows with no good-looking pieces.
What I think is a 'bug' is that Qt5 does not allow to derive from the QKdeTheme class by making its constructor private. This is troublesome, as Qt5 only uses the static createKdeTheme() method for creation - but there are no real reasons to disallow doing it manually. The only thing that createKdeTheme() does is fill in the two constructor parameters for the programmer.
I will try upstreaming a patch to fix this, as I think this design is wrong. The one-liner patch I made can be found here for now: patch.
But work is still ongoing. I will push the modified Qt5 and appmenu-qt5 packages to a PPA in the nearest days/weeks so that anyone that's interested can participate in testing. For now I got preempted to other tasks.
UPDATE: Forgot mentioning this earlier. The current solution is hosted on launchpad, you can find everything under the following links:
Project webpage: https://launchpad.net/appmenu-qt
Qt5 direct export approach branch: lp:~sil2100/appmenu-qt/appmenu-qt5-directexport
Feature bug: LP: #1157213