Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

highlighting custom QWidgetAction on hover

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

like image 524
pasbi Avatar asked Mar 10 '19 09:03

pasbi


1 Answers

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.

like image 52
thibsc Avatar answered Sep 19 '22 23:09

thibsc