A generic context menu class for Qt

published at 07.08.2015 16:07 by Jens Weller

I didn't plan to write a second post on menus. But a reply on twitter caused me to over think my code I presented yesterday. Its not an very important part, so that I moved on once it did run. So, the simple question, why I would not connect the menus to a slot instead of using a switch was a good one. It would restructure the code, and slots are also callable from the outside world, while the switch buries the code inside a method. Also you can reuse slots in making the functionality available in tool bars, window menus etc.

The video for this episode:

Why slots don't work in this case

While thinking on the problem, I realized that it would be that easy to fit the problem in a from that slots could be applied. The slot which triggers for a QAction is triggered(), or void(). But I have at least the QPoint in the showContextMenu slot I would need to propagate to the slot. I could introduce a member variable, and store the context inside, and then in the slot know from which context I'm called. But that seems to me error prone, as soon as I call the slot from a different context. Also, as this is a context menu, I do not have the use case to later connect it to a toolbar or having a window menu for deleting/creating items in the tree view.

But at that point, I understood, that I could go a different route, and get rid of the switch plus the enum for the different menu types. The data property of a QAction is a QVariant, which is able to store also generic types, if the type is known to the Qt meta type system. Custom (or standard types) may need to get a treatment with Q_DECLARE_METATYPE( type ) to actually work with QVariant. In yesterdays code, I did use the data property to store an int, which works out of the box. But, I could store anything in it, when its made known to the Qt metatype system via declare metatype. I'm not sure if you can stick a lambda in Q_DECLARE_METATYPE, it also would be no solution, as different lambdas are different types, and they do not share a common base. So, std::function is a pretty neat way to store callbacks, and an std::function has the needed interface for Q_DECLARE_METATYPE: public default constructor, destructor and copy constructor. So, this code sets up the usage of an std::function object for QVariant:

using menu_sig = std::function<void(QModelIndex& )>;
Q_DECLARE_METATYPE(menu_sig)

Now, this opens up to use a callback as the data member of a QAction. It was pretty simple to refactor everything in a way that it looked and worked great. But, the way I used a lambda to initialize each menu item in the code yesterday is actually a hack. Some times this helps me to spare my self from writing more boilerplate code, but its far from being optimal. I realized, that with further refactoring, only the block setting up the different menu items would be left. And I would have a generic context menu template. Also, Q_DECLARE_METATYPE could then easily in the template, oh wait, that didn't work. The above code has to be declared before you use the template, so that QVariant knows how to handle the signature type.

A generic context menu class

The basic pattern is easily explained, the type2menu member moves into a template, which gets as a template parameter the actual context signature, variadic templates make it possible to have any number of arguments in this, so this class is actually reusable when ever I need a context menu:

template< class context_sig, class hash_type = size_t>
class ContextMenu
{
    boost::container::flat_map<hash_type,QList<QAction*> > type2menu;
public:
    void registerAction(hash_type type_hash,const QString& text,const context_sig& sig, QObject* parent )
    template< class ...args>
    void displayMenu(hash_type type_hash,QPoint pos,args&&... a)
};

So, this template stores the type dependent menus in a flat_map, and the displayMenu method has to be a template method, to allow 0-n context parameters to be handed to the context_sig callback. Lets take a short look at registerAction first:

void registerAction(hash_type type_hash,const QString& text,const context_sig& sig, QObject* parent )
{
    QList<QAction*>& la = type2menu[type_hash];
    la.push_back(new QAction(text,parent));
    la.back()->setData(QVariant::fromValue<context_sig>(sig));
}

The first line could be deleted and the index operator do all the work. The only big difference to yesterday is, that QVariant now needs to know the specific type it stores, using its templated interface. The same interface is in the displayAction template method used:

template<class args...>
void displayMenu(hash_type type_hash,QPoint pos,args&&... a)
{
    if(type2menu.find(type_hash)== type2menu.end())//some items might have no submenu...
        return;
    auto action = QMenu::exec(type2menu[type_hash],pos);
    if(action)
        action->data(). template value< context_sig >()(std::forward<args>(a)...);
}

This is the calling code from the mainwindow class, which now simply calls the correct callback, once it has checked its availability, the parameters are automatically forwarded. Only thing left, is to actually instantiate and set up the context menu:

ContextMenu< menu_sig > context_menu; // in mainwindow.h
//in the MainWindow constructor:
auto delete_action = [this](QModelIndex& index)
{
    auto item = static_cast<ItemTreeModel::ItemPtr>(index.internalPointer());
    auto pwidget = factory.removeWidget(item->id(),item->type_id());
    if(pwidget)
    {
        int tabindex = ui->tabWidget->indexOf(pwidget);
        if(tabindex != -1)
            ui->tabWidget->removeTab(tabindex);
        pwidget->deleteLater();
    }
    treemodel->erase(index);
};
context_menu.registerAction(dir_typeid,"new Page",[this](QModelIndex& index ){createInstance< Page >(index,"Enter Page Name:");},this);
context_menu.registerAction(dir_typeid,"new Dir",[this](QModelIndex& index ){createInstance< Dir >(index,"Enter Directory Name:");},this);
context_menu.registerAction(dir_typeid,"delete Item",delete_action,this);
context_menu.registerAction(page_typeid,"delete Item",delete_action,this);

//the calling code: context_menu.displayMenu(item->type_id(),mapToGlobal(pos),index);

It is now very trivial to setup the context menu, and a lot of code could be deleted or moved in a type dedicated to being reused for a specific purpose. With this class, I never will need to write boilerplate code for context menus again. Well, I don't often use context menus, but maybe this changes now... ;)

Also, the code perfectly works the same way it did before. But is now much cleaner and shorter, as all the lines from the switch are gone.