Skip to content

Instantly share code, notes, and snippets.

@mpaperno
Last active January 25, 2025 05:59
Show Gist options
  • Save mpaperno/cd65ebf255945a6c1272fdf9e3c0746c to your computer and use it in GitHub Desktop.
Save mpaperno/cd65ebf255945a6c1272fdf9e3c0746c to your computer and use it in GitHub Desktop.
[Qt] An event filter to keep `QMenu` open after an action it contains has been triggered.
#pragma once
#include <QApplication>
#include <QKeyEvent>
#include <QMenu>
#include <QStyle>
/**
\class StayOpenMenuFilter
\brief An event filter to keep a `QMenu` open after an action it contains has been triggered.
When applied to a popup QMenu, will prevent closing the menu after an action is selected.
Meant to be used with `QObject::installEventFilter(new StayOpenMenuFilter())`, but also
has `installOnMenu()` convenience method, and will install itself if the `QObject *parent`
passed to c'tor is an instance of `QMenu`. E.g.:
```
QMenu *theMenu = new QMenu(this);
new StayOpenMenuFilter(theMenu);
```
By default it will prevent closing the menu when any action that doesn't have a sub-menu
is triggered. To only prevent closing when checkable actions are triggered, the public
member `onlyCheckableActions` can be set to `true` (as 2nd argument to the c'tor or separately).
Based on https://forum.qt.io/topic/13615/solved-how-to-keep-a-context-menu-open/5?_=1737767077720
and https://stackoverflow.com/a/75275070/3246449
*/
class StayOpenMenuFilter : public QObject
{
Q_OBJECT
public:
explicit StayOpenMenuFilter(QObject *parent = nullptr, bool onlyCheckableActions = false) :
QObject(parent),
onlyCheckableActions{onlyCheckableActions}
{
if (QMenu *m = qobject_cast<QMenu*>(parent))
installOnMenu(m);
}
bool onlyCheckableActions {false};
void installOnMenu(QMenu *w) {
w->installEventFilter(this);
connect(w, &QMenu::aboutToHide, this, [this]() { aboutToHide = true; });
connect(w, &QMenu::aboutToShow, this, [this]() { aboutToHide = false; });
}
protected:
bool eventFilter(QObject *obj, QEvent *ev) override
{
QMenu *menu;
QAction *activeAction;
if (
aboutToHide ||
!(menu = qobject_cast<QMenu*>(obj)) ||
!(activeAction = menu->activeAction()) ||
!activeAction->isEnabled() ||
activeAction->isSeparator() ||
!!activeAction->menu() ||
(onlyCheckableActions && !activeAction->isCheckable())
) {
return QObject::eventFilter(obj, ev);
}
bool filter = false;
if (ev->type() == QEvent::KeyPress) {
const int key = static_cast<QKeyEvent*>(ev)->key();
filter = (
key == Qt::Key_Return ||
key == Qt::Key_Enter ||
(key == Qt::Key_Space && QApplication::style()->styleHint(QStyle::SH_Menu_SpaceActivatesItem, nullptr, menu))
);
}
else if (ev->type() == QEvent::MouseButtonRelease) {
filter = true;
/* Technically on Windows only context menus can be activated with the right mouse button.
But determining what caused the menu is out of scope for now. See QMenu::mouseReleaseEvent() for example.
#ifdef Q_OS_WIN
filter = static_cast<QMouseEvent*>(ev)->button() == Qt::LeftButton || <is top level widget>;
#else
filter = true
#endif */
}
if (filter) {
activeAction->trigger();
ev->accept();
return true;
}
return QObject::eventFilter(obj, ev);
}
bool aboutToHide {false};
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment