diff --git a/src/plugins/platforms/cocoa/qcocoaeventdispatcher.h b/src/plugins/platforms/cocoa/qcocoaeventdispatcher.h index 69587a24be..b8d2532b8e 100644 --- a/src/plugins/platforms/cocoa/qcocoaeventdispatcher.h +++ b/src/plugins/platforms/cocoa/qcocoaeventdispatcher.h @@ -186,6 +186,7 @@ public: QAtomicInt serialNumber; int lastSerial; bool interrupt; + bool propagateInterrupt = false; static void postedEventsSourceCallback(void *info); static void waitingObserverCallback(CFRunLoopObserverRef observer, diff --git a/src/plugins/platforms/cocoa/qcocoaeventdispatcher.mm b/src/plugins/platforms/cocoa/qcocoaeventdispatcher.mm index 110c82bf1f..94b9e62eab 100644 --- a/src/plugins/platforms/cocoa/qcocoaeventdispatcher.mm +++ b/src/plugins/platforms/cocoa/qcocoaeventdispatcher.mm @@ -84,13 +84,12 @@ #include "private/qthread_p.h" #include "private/qguiapplication_p.h" #include +#include #include QT_BEGIN_NAMESPACE -QT_USE_NAMESPACE - static inline CFRunLoopRef mainRunLoop() { return CFRunLoopGetMain(); @@ -348,6 +347,16 @@ static inline void qt_mac_waitForMoreEvents(NSString *runLoopMode = NSDefaultRun bool QCocoaEventDispatcher::processEvents(QEventLoop::ProcessEventsFlags flags) { Q_D(QCocoaEventDispatcher); + + // In rare rather corner cases a user's application messes with + // QEventLoop::exec()/exit() and QCoreApplication::processEvents(), + // we have to undo what bool blocker normally does. + d->propagateInterrupt = false; + const auto boolBlockerUndo = qScopeGuard([d](){ + if (d->propagateInterrupt) + d->interrupt = true; + d->propagateInterrupt = false; + }); QBoolBlocker interruptBlocker(d->interrupt, false); bool interruptLater = false; @@ -496,7 +505,16 @@ bool QCocoaEventDispatcher::processEvents(QEventLoop::ProcessEventsFlags flags) if ((d->processEventsFlags & QEventLoop::EventLoopExec) == 0) { // When called "manually", always process posted events and timers + bool oldInterrupt = d->interrupt; d->processPostedEvents(); + if (!oldInterrupt && d->interrupt && !d->currentModalSession()) { + // We had direct processEvent call, coming not from QEventLoop::exec(). + // One of the posted events triggered an application to interrupt the loop. + // But bool blocker will reset d->interrupt to false, so the real event + // loop will never notice it was interrupted. Now we'll have to fix it by + // enforcing the value of d->interrupt. + d->propagateInterrupt = true; + } retVal = d->processTimers() || retVal; } diff --git a/tests/auto/corelib/kernel/qeventdispatcher/tst_qeventdispatcher.cpp b/tests/auto/corelib/kernel/qeventdispatcher/tst_qeventdispatcher.cpp index 49c10c6a24..365508024b 100644 --- a/tests/auto/corelib/kernel/qeventdispatcher/tst_qeventdispatcher.cpp +++ b/tests/auto/corelib/kernel/qeventdispatcher/tst_qeventdispatcher.cpp @@ -67,6 +67,7 @@ private slots: void sendPostedEvents_data(); void sendPostedEvents(); void processEventsOnlySendsQueuedEvents(); + void eventLoopExit(); }; bool tst_QEventDispatcher::event(QEvent *e) @@ -314,5 +315,49 @@ void tst_QEventDispatcher::processEventsOnlySendsQueuedEvents() QCOMPARE(object.eventsReceived, 4); } +void tst_QEventDispatcher::eventLoopExit() +{ + // This test was inspired by QTBUG-79477. A particular + // implementation detail in QCocoaEventDispatcher allowed + // QEventLoop::exit() to fail to really exit the event loop. + // Thus this test is a part of the dispatcher auto-test. + + // Imitates QApplication::exec(): + QEventLoop mainLoop; + // The test itself is a lambda: + QTimer::singleShot(0, [&mainLoop]() { + // Two more single shots, both will be posted as events + // (zero timeout) and supposed to be processes by the + // mainLoop: + + QTimer::singleShot(0, [&mainLoop]() { + // wakeUp triggers QCocoaEventDispatcher into incrementing + // its 'serialNumber': + mainLoop.wakeUp(); + // QCocoaEventDispatcher::processEvents() will process + // posted events and execute the second lambda defined below: + QCoreApplication::processEvents(); + }); + + QTimer::singleShot(0, [&mainLoop]() { + // With QCocoaEventDispatcher this is executed while in the + // processEvents (see above) and would fail to actually + // interrupt the loop. + mainLoop.exit(); + }); + }); + + bool timeoutObserved = false; + QTimer::singleShot(500, [&timeoutObserved, &mainLoop]() { + // In case the QEventLoop::exit above failed, we have to bail out + // early, not wasting time: + mainLoop.exit(); + timeoutObserved = true; + }); + + mainLoop.exec(); + QVERIFY(!timeoutObserved); +} + QTEST_MAIN(tst_QEventDispatcher) #include "tst_qeventdispatcher.moc"