highlighting custom QWidgetAction on hover
Asked Answered
D

2

7

My application has a QMenuBar with a number of QMenus, each of which having a number of QActions and sub-QMenus. Most of the QAction-items are derivatives of QWidgetAction with re-implemented QWidgetAction::createWidget methods.

Usually, both QActions and QMenu become highlighted on mouse hover. Even a QWidgetAction doesn't make trouble until here:

animated graphics of how highlighting is expected to work

But as soon as I override QWidgetAction::createWidget to return a custom QWidget

QWidget* MyWidgetAction::createWidget(QWidget* parent) { return new MyWidget(parent); }

the highlighting does not work anymore. So I implemented it myself:

void MyWidget::set_highlighted(bool h)
{
  setBackgroundRole(h ? QPalette::Highlight : QPalette::Window);
  setAutoFillBackground(h);
}
void MyWidget::enterEvent(QEvent*) override { set_highlighted(true); }
void MyWidget::leaveEvent(QEvent*) override { set_highlighted(false); }

However, it does not behave as expected:

animated graphics of what goes wrong with highlighting

I already figured out that the enterEvent method is not called until all sub-menus are closed, which only happens with some delay after mouse leaves the sub menu or its action (btw, how can I change the delay?). Same with mouse-move events.

Question: How can I re-implement highlight-on-hover properly? The user shall not notice that custom widget and standard QAction behave differently. What does the default QWidgetAction::createWidget do and how can I reproduce it? I've already looked at Qt's source but it's quite confusing.

Code to reproduce the animations

Actual production code

Dongdonga answered 10/3, 2019 at 9:59 Comment(3)
Does MyWidget override enterEvent and leaveEvent already or just as part of your attempt to solve the issue?Rasp
I only override them to solve the issue.Dongdonga
What about a keyboard navigation?Bonny
O
5

I think that the reason is you don't enable the mouse tracking on your widget, so the parent menu can't be notify that the mouse cursor change his position.

I suggest to add in the constructor of your MyWidget class this line:

setMousetracking(true);

Edit #1:
I found an ugly trick but it seems to be working:

// You WidgetAction class
class MyWidgetAction : public QWidgetAction
{
public:
    MyWidgetAction(QObject *parent = nullptr);
    QWidget* createWidget(QWidget* parent) override {
        w = new MyWidget(parent);
        return w;
    }
    void highlight(bool hl) { w->set_highlighted(hl); }

private:
    MyWidget *w;
};

// In your code
QMenu *menu = ui->menuBar->addMenu("The Menu");
menu->addAction("Standard QAction 1");
menu->addAction("Standard QAction 2");
menu->addMenu("submenu")->addAction("subaction1");
QWidgetAction *a = new MyWidgetAction();
a->setText("My action 1");
a->setParent(menu); // Needed for the trick
menu->addAction(a);
menu->addAction("Standard QAction 3");
menu->addAction("Standard QAction 4");

// The ugly trick
connect(menu, &QMenu::hovered, this, [menu](QAction *act){
    QList<MyWidgetAction*> lCustomActions = menu->findChildren<MyWidgetAction*>();
    for (MyWidgetAction *mwa : lCustomActions){
        mwa->highlight(mwa == act);
    }
});

I saw that the hovered signal is always send correctly, so I connect this to a lambda to check for each custom WidgetAction if it's the current hovered item and manually highlight in this case.


Edit #2:
To avoid the for loop in the lambda in my first edit, you can also create an eventfilter to manage the highlight on mouse move:

class WidgetActionFilterObject : public QObject
{
    Q_OBJECT
public:
    explicit WidgetActionFilterObject(QObject *parent = nullptr);

protected:
    bool eventFilter(QObject *obj, QEvent *evt) override {
        if (evt->type() == QEvent::Type::MouseMove){
            QMouseEvent *mouse_evt = static_cast<QMouseEvent*>(evt);
            QAction *a = static_cast<QMenu*>(obj)->actionAt(mouse_evt->pos());
            MyWidgetAction *mwa = dynamic_cast<MyWidgetAction*>(a);
            if (mwa){
                if (last_wa && mwa != last_wa){
                    last_wa->highlight(false);
                }
                mwa->highlight(true);
                last_wa = mwa;
            } else {
                if (last_wa){
                    last_wa->highlight(false);
                    last_wa = nullptr;
                }
            }
        }
        return QObject::eventFilter(obj, evt);
    }

private:
    MyWidgetAction *last_wa = nullptr;
};

Then the only thing you have to do is to install an event filter on each menu that contain your custom WidgetAction:

menu->installEventFilter(new WidgetActionFilterObject(this));

And you will obtain the same result without a loop on each hovered signal.

Operose answered 12/3, 2019 at 13:35 Comment(7)
I've already tried that. It does not solve the problem. In general, mouse-move, enter- and leave events are received, unless a sub-menu is shown. FYI: I once had enabled mouseTracking on each widget in the MWE, without any difference.Dongdonga
@Dongdonga , I have create a project to test and effectively, when a submenu is open, the behavior when the mouse come back on a QWidgetAction is special. I investigate ;)Operose
@pasbi, I have update my answer, it's not a beautiful solution but I have test and it's workingOperose
Thanks, that works indeed. I must have overlooked the QMenu::hovered signal. However, I agree with you that that's an ugly solution. I still hope for a proper solution.Dongdonga
The more I think about your solution, the more I feel it is the proper solution already. What should be wrong to make QMenu responsible for highlighting its actions? I think my desire to make each action widget responsible of its own highlighting is what is inappropriate...Dongdonga
@Dongdonga I just say it's ugly because we have to iterate over all MyWidgetAction of the menu to say if the highlight is on or not, but I think on this exemple, we can do a clever connect between the menu and the action to avoid the for loop. But the solution itself is not "bad"Operose
@pasbi, I make an edit, an other solution to have the choice ;)Operose
R
1

Note that this is not a complete answer (I'm not aiming for that bounty), but rather some solid background on QMenu and how it handles widgets.

You have two problems:

1) The default implementation of QWidgetAction::create() does nothing. You are intended to override this in your implementation, so it would be surprising if your original code did work.

2) Once you overrode QWidgetAction you ran into the larger issue: QMenu does not contain QWidgets with a QLayout. It's a container of pointers to QActions and a vector of their calculated QRects. It maintains state, but it's not a traditional Qt widget container.

"But wait!" you say, what about QMenuPrivate::widgetItems? Good question. QMenu does track the widgets added to it, but it's not tightly integrated. All QMenu really does is reserve the widget's sizeHint and ensure that it doesn't paint where a widget is probably located based on its sizeHint.

"I've already tried that. It does not solve the problem. In general, mouse-move, enter- and leave events are received, unless a sub-menu is shown. FYI: I once had enabled mouseTracking on each widget in the MWE, without any difference."

I mentioned above that QMenu doesn't really integrate with the widgets added to it? This is where it starts really telling. Sub-menus are platform-specific spaghetti code and tightly bound to QMenu's relevant QPA code. Things like the internal margins, the offset for painting the menu (on some platforms the submenu is offset by several pixels), which submenu currently has focus, all of these are not propagated to a QWidget added to a menu. Toss in a submenu that takes your focus (and creates its own event loop!) and suddenly you're in uncharted waters.

If you want to add a button to a single-level menu, QWidgetAction will do it. Actions that act like actions and display submenus? You're entering into "include qmenu_p.h" territory.

"The more I think about your solution, the more I feel it is the proper solution already. What should be wrong to make QMenu responsible for highlighting its actions? I think my desire to make each action widget responsible of its own highlighting is what is inappropriate..."

Correct. QMenu makes no special consideration for your widget. It just makes space for it and communicates primarily through its associated QAction.

Rasp answered 12/3, 2019 at 21:11 Comment(2)
Ok, that sounds solid. But: the original code did work. I mean that very piece where createWidget was not overridden and hence returned nullptr. The action does not look very exciting, but the highlighting works just fine (see first animation). But maybe I just want to accept that "Sub-menus are platform-specific spaghetti code" and don't care about the why anymore... Thanks for the insight, btw.Dongdonga
The original code worked insofar as a space was reserved for a widget and an action successfully added to the menu. It did not work in that there was, in fact, no widget there. Glad to help.Rasp

© 2022 - 2024 — McMap. All rights reserved.