QtWidgets: Fix enter/leave events on popup menus

If the sloppy menu popups - send the leave event to the last active
menu (except Cocoa), because only currect active menu gets enter/leave
events (currently Cocoa is an exception).

Check that the menu really has a mouse before hiding the sloppy menu -
don't rely on enter events.

This patch removes some unnecessary synthetic mouse enter/leave events
from QMenu which causes event duplications with different mouse cursor
position.

Refactor sloppy menu timer handling - start or restart timers on mouse
move events. Enter/leave events are not reliable.

Fixes:
- better enter/leave events handling for native widget actions,
- reduce duplicated enter/leave events for menu actions,
- better handle torn off sloppy menus.

Partially reverts: 0ed68f3f58
Amends: 57ecd5aeeb

Task-number: QTBUG-53068
Change-Id: I7ad56ac1619db124915d373fab82d0512d44c90e
Reviewed-by: Shawn Rutledge <shawn.rutledge@qt.io>
bb10
Błażej Szczygieł 2016-05-04 00:23:16 +02:00
parent 2cf3dee10b
commit 25c9a6c9b4
4 changed files with 74 additions and 74 deletions

View File

@ -381,7 +381,14 @@ void QWidgetWindow::handleEnterLeaveEvent(QEvent *event)
const QEnterEvent *ee = static_cast<QEnterEvent *>(event);
QWidget *child = m_widget->childAt(ee->pos());
QWidget *receiver = child ? child : m_widget.data();
QApplicationPrivate::dispatchEnterLeave(receiver, 0, ee->screenPos());
QWidget *leave = Q_NULLPTR;
if (QApplicationPrivate::inPopupMode() && receiver == m_widget
&& qt_last_mouse_receiver != m_widget) {
// This allows to deliver the leave event to the native widget
// action on first-level menu.
leave = qt_last_mouse_receiver;
}
QApplicationPrivate::dispatchEnterLeave(receiver, leave, ee->screenPos());
qt_last_mouse_receiver = receiver;
}
}
@ -471,34 +478,31 @@ void QWidgetWindow::handleMouseEvent(QMouseEvent *event)
receiver = popupChild;
if (receiver != activePopupWidget)
widgetPos = receiver->mapFromGlobal(event->globalPos());
QWidget *alien = receiver;
#if !defined(Q_OS_OSX) && !defined(Q_OS_IOS) // Cocoa tracks popups
const bool reallyUnderMouse = activePopupWidget->rect().contains(mapped);
const bool underMouse = activePopupWidget->underMouse();
if (activePopupWidget != m_widget || (!underMouse && qt_button_down)) {
// If active popup menu is not the first-level popup menu then we must emulate enter/leave events,
// because first-level popup menu grabs the mouse and enter/leave events are delivered only to it
// by QPA. Make an exception for first-level popup menu when the mouse button is pressed on widget.
if (underMouse != reallyUnderMouse) {
if (reallyUnderMouse) {
if (underMouse != reallyUnderMouse) {
if (reallyUnderMouse) {
const QPoint receiverMapped = receiver->mapFromGlobal(event->screenPos().toPoint());
// Prevent negative mouse position on enter event - this event
// should be properly handled in "handleEnterLeaveEvent()".
if (receiverMapped.x() >= 0 && receiverMapped.y() >= 0) {
QApplicationPrivate::dispatchEnterLeave(receiver, Q_NULLPTR, event->screenPos());
qt_last_mouse_receiver = receiver;
} else {
QApplicationPrivate::dispatchEnterLeave(Q_NULLPTR, qt_last_mouse_receiver, event->screenPos());
qt_last_mouse_receiver = receiver;
receiver = activePopupWidget;
}
} else {
QApplicationPrivate::dispatchEnterLeave(Q_NULLPTR, qt_last_mouse_receiver, event->screenPos());
qt_last_mouse_receiver = receiver;
receiver = activePopupWidget;
}
} else if (!reallyUnderMouse) {
alien = Q_NULLPTR;
}
#endif
QMouseEvent e(event->type(), widgetPos, event->windowPos(), event->screenPos(),
event->button(), event->buttons(), event->modifiers(), event->source());
e.setTimestamp(event->timestamp());
QApplicationPrivate::sendMouseEvent(receiver, &e, alien, receiver->window(), &qt_button_down, qt_last_mouse_receiver);
QApplicationPrivate::sendMouseEvent(receiver, &e, receiver, receiver->window(), &qt_button_down, qt_last_mouse_receiver);
qt_last_mouse_receiver = receiver;
} else {
// close disabled popups when a mouse button is pressed or released

View File

@ -70,22 +70,6 @@
QT_BEGIN_NAMESPACE
QMenu *QMenuPrivate::mouseDown = 0;
QPointer<QMenu> QMenuPrivate::previousMouseMenu(Q_NULLPTR);
static void handleEnterLeaveEvents(QPointer<QMenu> *previous_ptr, QMenu *next)
{
QWidget *previous = previous_ptr->data();
if (previous != next) {
if (previous) {
QEvent leaveEvent(QEvent::Leave);
QApplication::sendEvent(previous, &leaveEvent);
}
if (next) {
QEvent enterEvent(QEvent::Enter);
QApplication::sendEvent(next, &enterEvent);
}
}
*previous_ptr = next;
}
/* QMenu code */
// internal class used for the torn off popup
@ -504,8 +488,6 @@ void QMenuPrivate::hideMenu(QMenu *menu)
menu->d_func()->causedPopup.action = 0;
menu->close();
menu->d_func()->causedPopup.widget = 0;
if (previousMouseMenu.data() == menu)
handleEnterLeaveEvents(&previousMouseMenu, Q_NULLPTR);
}
void QMenuPrivate::popupAction(QAction *action, int delay, bool activateFirst)
@ -671,10 +653,26 @@ void QMenuSloppyState::enter()
m_parent->childEnter();
}
void QMenuSloppyState::childEnter()
{
stopTimer();
if (m_parent)
m_parent->childEnter();
}
void QMenuSloppyState::leave()
{
if (!m_dont_start_time_on_leave) {
if (m_parent)
m_parent->childLeave();
startTimerIfNotRunning();
}
}
void QMenuSloppyState::childLeave()
{
if (m_enabled && !QMenuPrivate::get(m_menu)->hasReceievedEnter) {
startTimer();
startTimerIfNotRunning();
if (m_parent)
m_parent->childLeave();
}
@ -720,8 +718,17 @@ public:
void QMenuSloppyState::timeout()
{
QMenuPrivate *menu_priv = QMenuPrivate::get(m_menu);
bool reallyHasMouse = menu_priv->hasReceievedEnter;
if (!reallyHasMouse) {
// Check whether the menu really has a mouse, because only active popup
// menu gets the enter/leave events. Currently Cocoa is an exception.
const QPoint lastCursorPos = QGuiApplicationPrivate::lastCursorPosition.toPoint();
reallyHasMouse = m_menu->frameGeometry().contains(lastCursorPos);
}
if (menu_priv->currentAction == m_reset_action
&& menu_priv->hasReceievedEnter
&& reallyHasMouse
&& (menu_priv->currentAction
&& menu_priv->currentAction->menu() == menu_priv->activeMenu)) {
return;
@ -729,13 +736,13 @@ void QMenuSloppyState::timeout()
ResetOnDestroy resetState(this, &m_init_guard);
if (hasParentActiveDelayTimer() || !m_menu || !m_menu->isVisible())
if (hasParentActiveDelayTimer() || !m_menu->isVisible())
return;
if (m_sub_menu)
menu_priv->hideMenu(m_sub_menu);
if (menu_priv->hasReceievedEnter)
if (reallyHasMouse)
menu_priv->setCurrentAction(m_reset_action,0);
else
menu_priv->setCurrentAction(Q_NULLPTR, 0);
@ -1089,10 +1096,8 @@ bool QMenuPrivate::mouseEventTaken(QMouseEvent *e)
tearoffHighlighted = 0;
}
if (q->frameGeometry().contains(e->globalPos())) { //otherwise if the event is in our rect we want it..
handleEnterLeaveEvents(&previousMouseMenu, q);
return false;
}
if (q->frameGeometry().contains(e->globalPos()))
return false; //otherwise if the event is in our rect we want it..
for(QWidget *caused = causedPopup.widget; caused;) {
bool passOnEvent = false;
@ -1108,17 +1113,16 @@ bool QMenuPrivate::mouseEventTaken(QMouseEvent *e)
next_widget = m->d_func()->causedPopup.widget;
}
if (passOnEvent) {
handleEnterLeaveEvents(&previousMouseMenu,qobject_cast<QMenu *>(caused));
if(e->type() != QEvent::MouseButtonRelease || mouseDown == caused) {
QMouseEvent new_e(e->type(), cpos, caused->mapTo(caused->topLevelWidget(), cpos), e->screenPos(),
e->button(), e->buttons(), e->modifiers(), e->source());
QApplication::sendEvent(caused, &new_e);
return true;
if (e->type() != QEvent::MouseButtonRelease || mouseDown == caused) {
QMouseEvent new_e(e->type(), cpos, caused->mapTo(caused->topLevelWidget(), cpos), e->screenPos(),
e->button(), e->buttons(), e->modifiers(), e->source());
QApplication::sendEvent(caused, &new_e);
return true;
}
}
caused = next_widget;
if (!caused)
handleEnterLeaveEvents(&previousMouseMenu, Q_NULLPTR);
sloppyState.leave(); // Start timers
}
return false;
}
@ -3169,7 +3173,6 @@ void QMenu::enterEvent(QEvent *)
Q_D(QMenu);
d->hasReceievedEnter = true;
d->sloppyState.enter();
d->sloppyState.startTimer();
d->motions = -1; // force us to ignore the generate mouse move in mouseMoveEvent()
}
@ -3180,7 +3183,6 @@ void QMenu::leaveEvent(QEvent *)
{
Q_D(QMenu);
d->hasReceievedEnter = false;
d->sloppyState.leave();
if (!d->activeMenu && d->currentAction)
setActiveAction(0);
}
@ -3352,10 +3354,18 @@ void QMenu::internalDelayedPopup()
const QRect actionRect(d->actionRect(d->currentAction));
const QPoint rightPos(mapToGlobal(QPoint(actionRect.right() + subMenuOffset + 1, actionRect.top())));
QPoint pos(rightPos);
d->activeMenu->popup(pos);
d->activeMenu->popup(rightPos);
d->sloppyState.setSubMenuPopup(actionRect, d->currentAction, d->activeMenu);
#if !defined(Q_OS_DARWIN)
// Send the leave event to the current menu - only active popup menu gets
// mouse enter/leave events. Currently Cocoa is an exception, so disable
// it there to avoid event duplication.
if (underMouse()) {
QEvent leaveEvent(QEvent::Leave);
QCoreApplication::sendEvent(this, &leaveEvent);
}
#endif
}
/*!

View File

@ -122,8 +122,6 @@ public:
void reset();
bool enabled() const { return m_enabled; }
void setResetAction(QAction *action) { m_reset_action = action; }
enum MouseEventResult {
EventIsProcessed,
EventShouldBePropagated,
@ -148,22 +146,9 @@ public:
}
void enter();
void childEnter();
void childEnter()
{
stopTimer();
if (m_parent)
m_parent->childEnter();
}
void leave()
{
if (m_dont_start_time_on_leave)
return;
if (m_parent)
m_parent->childLeave();
startTimer();
}
void leave();
void childLeave();
static float slope(const QPointF &p1, const QPointF &p2)
@ -189,8 +174,7 @@ public:
if (!m_enabled)
return EventShouldBePropagated;
if (!m_time.isActive())
startTimer();
startTimerIfNotRunning();
if (!m_sub_menu) {
reset();
@ -493,7 +477,6 @@ public:
QAction* wceCommands(uint command);
#endif
QPointer<QWidget> noReplayFor;
static QPointer<QMenu> previousMouseMenu;
};
#endif // QT_NO_MENU

View File

@ -10358,8 +10358,11 @@ void tst_QWidget::underMouse()
QCOMPARE(childWidget2.leaves, 0);
// Mouse leaves popup and enters topLevelWidget, should cause leave for popup
// but no enter to topLevelWidget. Again, artificial leave event needed.
// but no enter to topLevelWidget.
#ifdef Q_OS_DARWIN
// Artificial leave event needed for Cocoa.
QWindowSystemInterface::handleLeaveEvent(popupWindow);
#endif
QTest::mouseMove(popupWindow, popupWindow->mapFromGlobal(window->mapToGlobal(inWindowPoint)));
QApplication::processEvents();
QVERIFY(!topLevelWidget.underMouse());