Revision control

Copy as Markdown

// Copyright (C) 2016 The Qt Company Ltd.
// Copyright (c) 2007-2008, Apple, Inc.
// SPDX-License-Identifier: BSD-3-Clause
// Qt-Security score:significant reason:default
#include <AppKit/AppKit.h>
#include "qcocoaeventdispatcher.h"
#include "qcocoawindow.h"
#include "qcocoahelpers.h"
#include <QtGui/qevent.h>
#include <QtGui/qguiapplication.h>
#include <QtGui/private/qguiapplication_p.h>
#include <QtCore/qmutex.h>
#include <QtCore/qscopeguard.h>
#include <QtCore/qsocketnotifier.h>
#include <QtCore/private/qthread_p.h>
#include <QtCore/private/qcore_mac_p.h>
#include <qpa/qplatformwindow.h>
#include <qpa/qplatformnativeinterface.h>
#include <QtCore/qdebug.h>
QT_BEGIN_NAMESPACE
Q_LOGGING_CATEGORY(lcEventDispatcher, "qt.eventdispatcher");
static inline CFRunLoopRef mainRunLoop()
{
return CFRunLoopGetMain();
}
static Boolean runLoopSourceEqualCallback(const void *info1, const void *info2)
{
return info1 == info2;
}
/*****************************************************************************
Timers stuff
*****************************************************************************/
/* timer call back */
void QCocoaEventDispatcherPrivate::runLoopTimerCallback(CFRunLoopTimerRef, void *info)
{
QCocoaEventDispatcherPrivate *d = static_cast<QCocoaEventDispatcherPrivate *>(info);
if (d->processEventsCalled && (d->processEventsFlags & QEventLoop::EventLoopExec) == 0) {
// processEvents() was called "manually," ignore this source for now
d->maybeCancelWaitForMoreEvents();
return;
}
CFRunLoopSourceSignal(d->activateTimersSourceRef);
}
void QCocoaEventDispatcherPrivate::activateTimersSourceCallback(void *info)
{
QCocoaEventDispatcherPrivate *d = static_cast<QCocoaEventDispatcherPrivate *>(info);
if (d->initializingNSApplication) {
qCDebug(lcEventDispatcher) << "Deferring" << __FUNCTION__ << "due to NSApp initialization";
// We don't want to process any sources during explicit NSApplication
// initialization, so defer the source until the actual event processing.
CFRunLoopSourceSignal(d->activateTimersSourceRef);
return;
}
d->processTimers();
d->maybeCancelWaitForMoreEvents();
}
bool QCocoaEventDispatcherPrivate::processTimers()
{
int activated = timerInfoList.activateTimers();
maybeStartCFRunLoopTimer();
return activated > 0;
}
void QCocoaEventDispatcherPrivate::maybeStartCFRunLoopTimer()
{
if (timerInfoList.isEmpty()) {
// no active timers, so the CFRunLoopTimerRef should not be active either
Q_ASSERT(!runLoopTimerRef);
return;
}
using DoubleSeconds = std::chrono::duration<double, std::ratio<1>>;
if (!runLoopTimerRef) {
// start the CFRunLoopTimer
CFAbsoluteTime ttf = CFAbsoluteTimeGetCurrent();
CFTimeInterval interval;
CFTimeInterval oneyear = CFTimeInterval(3600. * 24. * 365.);
// Q: when should the CFRunLoopTimer fire for the first time?
if (auto opt = timerInfoList.timerWait()) {
// A: when we have timers to fire, of course
DoubleSeconds secs{*opt};
interval = qMax(secs.count(), 0.0000001);
} else {
// this shouldn't really happen, but in case it does, set the timer to fire a some point in the distant future
interval = oneyear;
}
ttf += interval;
CFRunLoopTimerContext info = { 0, this, nullptr, nullptr, nullptr };
// create the timer with a large interval, as recommended by the CFRunLoopTimerSetNextFireDate()
// documentation, since we will adjust the timer's time-to-fire as needed to keep Qt timers working
runLoopTimerRef = CFRunLoopTimerCreate(nullptr, ttf, oneyear, 0, 0, QCocoaEventDispatcherPrivate::runLoopTimerCallback, &info);
Q_ASSERT(runLoopTimerRef);
CFRunLoopAddTimer(mainRunLoop(), runLoopTimerRef, kCFRunLoopCommonModes);
} else {
// calculate when we need to wake up to process timers again
CFAbsoluteTime ttf = CFAbsoluteTimeGetCurrent();
CFTimeInterval interval;
// Q: when should the timer first next?
if (auto opt = timerInfoList.timerWait()) {
// A: when we have timers to fire, of course
DoubleSeconds secs{*opt};
interval = qMax(secs.count(), 0.0000001);
} else {
// no timers can fire, but we cannot stop the CFRunLoopTimer, set the timer to fire at some
// point in the distant future (the timer interval is one year)
interval = CFRunLoopTimerGetInterval(runLoopTimerRef);
}
ttf += interval;
CFRunLoopTimerSetNextFireDate(runLoopTimerRef, ttf);
}
}
void QCocoaEventDispatcherPrivate::maybeStopCFRunLoopTimer()
{
if (!runLoopTimerRef)
return;
CFRunLoopTimerInvalidate(runLoopTimerRef);
CFRelease(runLoopTimerRef);
runLoopTimerRef = nullptr;
}
void QCocoaEventDispatcher::registerTimer(Qt::TimerId timerId, Duration interval,
Qt::TimerType timerType, QObject *obj)
{
#ifndef QT_NO_DEBUG
if (qToUnderlying(timerId) < 1 || interval.count() < 0 || !obj) {
qWarning("QCocoaEventDispatcher::registerTimer: invalid arguments");
return;
} else if (obj->thread() != thread() || thread() != QThread::currentThread()) {
qWarning("QObject::startTimer: timers cannot be started from another thread");
return;
}
#endif
Q_D(QCocoaEventDispatcher);
d->timerInfoList.registerTimer(timerId, interval, timerType, obj);
d->maybeStartCFRunLoopTimer();
}
bool QCocoaEventDispatcher::unregisterTimer(Qt::TimerId timerId)
{
#ifndef QT_NO_DEBUG
if (qToUnderlying(timerId) < 1) {
qWarning("QCocoaEventDispatcher::unregisterTimer: invalid argument");
return false;
} else if (thread() != QThread::currentThread()) {
qWarning("QObject::killTimer: timers cannot be stopped from another thread");
return false;
}
#endif
Q_D(QCocoaEventDispatcher);
bool returnValue = d->timerInfoList.unregisterTimer(timerId);
if (!d->timerInfoList.isEmpty())
d->maybeStartCFRunLoopTimer();
else
d->maybeStopCFRunLoopTimer();
return returnValue;
}
bool QCocoaEventDispatcher::unregisterTimers(QObject *obj)
{
#ifndef QT_NO_DEBUG
if (!obj) {
qWarning("QCocoaEventDispatcher::unregisterTimers: invalid argument");
return false;
} else if (obj->thread() != thread() || thread() != QThread::currentThread()) {
qWarning("QObject::killTimers: timers cannot be stopped from another thread");
return false;
}
#endif
Q_D(QCocoaEventDispatcher);
bool returnValue = d->timerInfoList.unregisterTimers(obj);
if (!d->timerInfoList.isEmpty())
d->maybeStartCFRunLoopTimer();
else
d->maybeStopCFRunLoopTimer();
return returnValue;
}
QList<QCocoaEventDispatcher::TimerInfoV2>
QCocoaEventDispatcher::timersForObject(QObject *object) const
{
#ifndef QT_NO_DEBUG
if (!object) {
qWarning("QCocoaEventDispatcher:registeredTimers: invalid argument");
return {};
}
#endif
Q_D(const QCocoaEventDispatcher);
return d->timerInfoList.registeredTimers(object);
}
/*
Register a QSocketNotifier with the mac event system by creating a CFSocket with
with a read/write callback.
Qt has separate socket notifiers for reading and writing, but on the mac there is
a limitation of one CFSocket object for each native socket.
*/
void QCocoaEventDispatcher::registerSocketNotifier(QSocketNotifier *notifier)
{
Q_D(QCocoaEventDispatcher);
d->cfSocketNotifier.registerSocketNotifier(notifier);
}
void QCocoaEventDispatcher::unregisterSocketNotifier(QSocketNotifier *notifier)
{
Q_D(QCocoaEventDispatcher);
d->cfSocketNotifier.unregisterSocketNotifier(notifier);
}
static bool isUserInputEvent(NSEvent* event)
{
switch ([event type]) {
case NSEventTypeLeftMouseDown:
case NSEventTypeLeftMouseUp:
case NSEventTypeRightMouseDown:
case NSEventTypeRightMouseUp:
case NSEventTypeMouseMoved: // ??
case NSEventTypeLeftMouseDragged:
case NSEventTypeRightMouseDragged:
case NSEventTypeMouseEntered:
case NSEventTypeMouseExited:
case NSEventTypeKeyDown:
case NSEventTypeKeyUp:
case NSEventTypeFlagsChanged: // key modifiers changed?
case NSEventTypeCursorUpdate: // ??
case NSEventTypeScrollWheel:
case NSEventTypeTabletPoint:
case NSEventTypeTabletProximity:
case NSEventTypeOtherMouseDown:
case NSEventTypeOtherMouseUp:
case NSEventTypeOtherMouseDragged:
#ifndef QT_NO_GESTURES
case NSEventTypeGesture: // touch events
case NSEventTypeMagnify:
case NSEventTypeSwipe:
case NSEventTypeRotate:
case NSEventTypeBeginGesture:
case NSEventTypeEndGesture:
#endif // QT_NO_GESTURES
return true;
break;
default:
break;
}
return false;
}
static inline void qt_mac_waitForMoreEvents(NSString *runLoopMode = NSDefaultRunLoopMode)
{
// If no event exist in the cocoa event que, wait (and free up cpu time) until
// at least one event occur. Setting 'dequeuing' to 'no' in the following call
// causes it to hang under certain circumstances (QTBUG-28283), so we tell it
// to dequeue instead, just to repost the event again:
NSEvent* event = [NSApp nextEventMatchingMask:NSEventMaskAny
untilDate:[NSDate distantFuture]
inMode:runLoopMode
dequeue:YES];
if (event)
[NSApp postEvent:event atStart:YES];
}
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;
});
QScopedValueRollback interruptBlocker(d->interrupt, false);
bool interruptLater = false;
QtCocoaInterruptDispatcher::cancelInterruptLater();
emit awake();
uint oldflags = d->processEventsFlags;
d->processEventsFlags = flags;
// Used to determine whether any eventloop has been exec'ed, and allow posted
// and timer events to be processed even if this function has never been called
// instead of being kept on hold for the next run of processEvents().
++d->processEventsCalled;
bool excludeUserEvents = d->processEventsFlags & QEventLoop::ExcludeUserInputEvents;
bool retVal = false;
forever {
if (d->interrupt)
break;
QMacAutoReleasePool pool;
NSEvent* event = nil;
// First, send all previously excluded input events, if any:
if (d->sendQueuedUserInputEvents())
retVal = true;
// If Qt is used as a plugin, or as an extension in a native cocoa
// application, we should not run or stop NSApplication; This will be
// done from the application itself. And if processEvents is called
// manually (rather than from a QEventLoop), we cannot enter a tight
// loop and block this call, but instead we need to return after one flush.
// Finally, if we are to exclude user input events, we cannot call [NSApp run]
// as we then loose control over which events gets dispatched:
const bool canExec_3rdParty = d->nsAppRunCalledByQt || ![NSApp isRunning];
const bool canExec_Qt = (!excludeUserEvents
&& ((d->processEventsFlags & QEventLoop::DialogExec)
|| (d->processEventsFlags & QEventLoop::EventLoopExec)));
if (canExec_Qt && canExec_3rdParty) {
// We can use exec-mode, meaning that we can stay in a tight loop until
// interrupted. This is mostly an optimization, but it allow us to use
// [NSApp run], which is the normal code path for cocoa applications.
if (NSModalSession session = d->currentModalSession()) {
QScopedValueRollback execGuard(d->currentExecIsNSAppRun, false);
qCDebug(lcEventDispatcher) << "Running modal session" << session;
while ([NSApp runModalSession:session] == NSModalResponseContinue && !d->interrupt) {
qt_mac_waitForMoreEvents(NSModalPanelRunLoopMode);
if (session != d->currentModalSessionCached) {
// It's possible to release the current modal session
// while we are in this loop, for example, by closing all
// windows from a slot via QApplication::closeAllWindows.
// In this case we cannot use 'session' anymore. A warning
// from Cocoa is: "Use of freed session detected. Do not
// call runModalSession: after calling endModalSesion:."
break;
}
}
if (!d->interrupt && session == d->currentModalSessionCached) {
// Someone called [NSApp stopModal:] from outside the event
// dispatcher (e.g to stop a native dialog). But that call wrongly stopped
// 'session' as well. As a result, we need to restart all internal sessions:
d->temporarilyStopAllModalSessions();
}
// Clean up the modal session list, call endModalSession.
if (d->cleanupModalSessionsNeeded)
d->cleanupModalSessions();
} else {
d->nsAppRunCalledByQt = true;
QScopedValueRollback execGuard(d->currentExecIsNSAppRun, true);
[NSApp run];
}
retVal = true;
} else {
int lastSerialCopy = d->lastSerial;
const bool hadModalSession = d->currentModalSessionCached;
// We cannot block the thread (and run in a tight loop).
// Instead we will process all current pending events and return.
d->ensureNSAppInitialized();
if (NSModalSession session = d->currentModalSession()) {
// INVARIANT: a modal window is executing.
if (!excludeUserEvents) {
// Since we can dispatch all kinds of events, we choose
// to use cocoa's native way of running modal sessions:
if (flags & QEventLoop::WaitForMoreEvents)
qt_mac_waitForMoreEvents(NSModalPanelRunLoopMode);
qCDebug(lcEventDispatcher) << "Running modal session" << session;
NSInteger status = [NSApp runModalSession:session];
if (status != NSModalResponseContinue && session == d->currentModalSessionCached) {
// INVARIANT: Someone called [NSApp stopModal:] from outside the event
// dispatcher (e.g to stop a native dialog). But that call wrongly stopped
// 'session' as well. As a result, we need to restart all internal sessions:
d->temporarilyStopAllModalSessions();
}
// Clean up the modal session list, call endModalSession.
if (d->cleanupModalSessionsNeeded)
d->cleanupModalSessions();
retVal = true;
} else do {
// Dispatch all non-user events (but que non-user events up for later). In
// this case, we need more control over which events gets dispatched, and
// cannot use [NSApp runModalSession:session]:
event = [NSApp nextEventMatchingMask:NSEventMaskAny
untilDate:nil
inMode:NSModalPanelRunLoopMode
dequeue: YES];
if (event) {
if (isUserInputEvent(event)) {
[event retain];
d->queuedUserInputEvents.append(event);
continue;
}
if (!filterNativeEvent("NSEvent", event, nullptr)) {
[NSApp sendEvent:event];
retVal = true;
}
}
} while (!d->interrupt && event);
} else do {
// INVARIANT: No modal window is executing.
event = [NSApp nextEventMatchingMask:NSEventMaskAny
untilDate:nil
inMode:NSDefaultRunLoopMode
dequeue: YES];
if (event) {
if (flags & QEventLoop::ExcludeUserInputEvents) {
if (isUserInputEvent(event)) {
[event retain];
d->queuedUserInputEvents.append(event);
continue;
}
}
if (!filterNativeEvent("NSEvent", event, nullptr)) {
[NSApp sendEvent:event];
retVal = true;
}
}
// Clean up the modal session list, call endModalSession.
if (d->cleanupModalSessionsNeeded)
d->cleanupModalSessions();
} while (!d->interrupt && event);
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;
}
// be sure to return true if the posted event source fired
retVal = retVal || lastSerialCopy != d->lastSerial;
// Since the window that holds modality might have changed while processing
// events, we we need to interrupt when we return back the previous process
// event recursion to ensure that we spin the correct modal session.
// We do the interruptLater at the end of the function to ensure that we don't
// disturb the 'wait for more events' below (as deleteLater will post an event):
if (hadModalSession && !d->currentModalSessionCached)
interruptLater = true;
}
bool canWait = (d->threadData.loadRelaxed()->canWait
&& !retVal
&& !d->interrupt
&& (d->processEventsFlags & QEventLoop::WaitForMoreEvents));
if (canWait) {
// INVARIANT: We haven't processed any events yet. And we're told
// to stay inside this function until at least one event is processed.
qt_mac_waitForMoreEvents();
d->processEventsFlags &= ~QEventLoop::WaitForMoreEvents;
} else {
// Done with event processing for now.
// Leave the function:
break;
}
}
d->processEventsFlags = oldflags;
--d->processEventsCalled;
// If we're interrupted, we need to interrupt the _current_
// recursion as well to check if it is still supposed to be
// executing. This way we wind down the stack until we land
// on a recursion that again calls processEvents (typically
// from QEventLoop), and set interrupt to false:
if (d->interrupt)
interrupt();
if (interruptLater)
QtCocoaInterruptDispatcher::interruptLater();
return retVal;
}
auto QCocoaEventDispatcher::remainingTime(Qt::TimerId timerId) const -> Duration
{
#ifndef QT_NO_DEBUG
if (qToUnderlying(timerId) < 1) {
qWarning("QCocoaEventDispatcher::remainingTime: invalid argument");
return Duration::min();
}
#endif
Q_D(const QCocoaEventDispatcher);
return d->timerInfoList.remainingDuration(timerId);
}
void QCocoaEventDispatcher::wakeUp()
{
Q_D(QCocoaEventDispatcher);
d->serialNumber.ref();
CFRunLoopSourceSignal(d->postedEventsSource);
CFRunLoopWakeUp(mainRunLoop());
}
/*****************************************************************************
QEventDispatcherMac Implementation
*****************************************************************************/
void QCocoaEventDispatcherPrivate::ensureNSAppInitialized()
{
// Some elements in Cocoa require NSApplication to be initialized before
// use, for example the menu bar. Under normal circumstances this happens
// as part of [NSApp run], as a result of a call to QGuiApplication:exec(),
// but in the cases where a dialog is asked to execute before that happens,
// or the application spins the event loop manually via processEvents(),
// we need to explicitly ensure NSApplication initialization.
// We can unfortunately not do this via NSApplicationLoad(), as the function
// bails out early if there's already an NSApplication instance, which is
// the case if any code has called [NSApplication sharedApplication],
// or its short form 'NSApp'.
// Instead we do an actual [NSApp run], but stop the application as soon
// as possible, ensuring that AppKit will do the required initialization,
// including calling [NSApplication finishLaunching].
// We only apply this trick at most once for any application, and we avoid
// doing it for the common case where main just starts QGuiApplication::exec.
if (nsAppRunCalledByQt || [NSApp isRunning])
return;
qCDebug(lcEventDispatcher) << "Ensuring NSApplication is initialized";
nsAppRunCalledByQt = true;
// Stopping the application will still process runloop sources before
// actually stopping, so we need to explicitly guard our sources from
// doing anything, deferring their actions until later.
QScopedValueRollback initializationGuard(initializingNSApplication, true);
CFRunLoopPerformBlock(mainRunLoop(), kCFRunLoopCommonModes, ^{
qCDebug(lcEventDispatcher) << "NSApplication has been initialized; Stopping NSApp";
[NSApp stop:NSApp];
cancelWaitForMoreEvents(); // Post event that wakes up the runloop
});
[NSApp run];
qCDebug(lcEventDispatcher) << "Finished ensuring NSApplication is initialized";
}
void QCocoaEventDispatcherPrivate::temporarilyStopAllModalSessions()
{
// Flush, and Stop, all created modal session, and as
// such, make them pending again. The next call to
// currentModalSession will recreate them again. The
// reason to stop all session like this is that otherwise
// a call [NSApp stop] would not stop NSApp, but rather
// the current modal session. So if we need to stop NSApp
// we need to stop all the modal session first. To avoid changing
// the stacking order of the windows while doing so, we put
// up a block that is used in QCocoaWindow and QCocoaPanel:
int stackSize = cocoaModalSessionStack.size();
for (int i=0; i<stackSize; ++i) {
QCocoaModalSessionInfo &info = cocoaModalSessionStack[i];
if (info.session) {
qCDebug(lcEventDispatcher) << "Temporarily ending modal session" << info.session
<< "for" << info.nswindow;
[NSApp endModalSession:info.session];
info.session = nullptr;
[(NSWindow*) info.nswindow release];
}
}
currentModalSessionCached = nullptr;
}
NSModalSession QCocoaEventDispatcherPrivate::currentModalSession()
{
// If we have one or more modal windows, this function will create
// a session for each of those, and return the one for the top.
if (currentModalSessionCached)
return currentModalSessionCached;
if (cocoaModalSessionStack.isEmpty())
return nullptr;
int sessionCount = cocoaModalSessionStack.size();
for (int i=0; i<sessionCount; ++i) {
QCocoaModalSessionInfo &info = cocoaModalSessionStack[i];
if (!info.window)
continue;
if (!info.session) {
QMacAutoReleasePool pool;
QCocoaWindow *cocoaWindow = static_cast<QCocoaWindow *>(info.window->handle());
if (!cocoaWindow)
continue;
NSWindow *nswindow = cocoaWindow->nativeWindow();
if (!nswindow)
continue;
ensureNSAppInitialized();
QScopedValueRollback block1(blockSendPostedEvents, true);
info.nswindow = nswindow;
[(NSWindow*) info.nswindow retain];
QRect rect = cocoaWindow->geometry();
info.session = [NSApp beginModalSessionForWindow:nswindow];
qCDebug(lcEventDispatcher) << "Begun modal session" << info.session
<< "for" << nswindow;
// The call to beginModalSessionForWindow above processes events and may
// have deleted or destroyed the window. Check if it's still valid.
if (!info.window)
continue;
cocoaWindow = static_cast<QCocoaWindow *>(info.window->handle());
if (!cocoaWindow)
continue;
if (rect != cocoaWindow->geometry())
cocoaWindow->setGeometry(rect);
}
currentModalSessionCached = info.session;
cleanupModalSessionsNeeded = false;
}
return currentModalSessionCached;
}
bool QCocoaEventDispatcherPrivate::hasModalSession() const
{
return !cocoaModalSessionStack.isEmpty();
}
void QCocoaEventDispatcherPrivate::cleanupModalSessions()
{
// Go through the list of modal sessions, and end those
// that no longer has a window associated; no window means
// the session has logically ended. The reason we wait like
// this to actually end the sessions for real (rather than at the
// point they were marked as stopped), is that ending a session
// when no other session runs below it on the stack will make cocoa
// drop some events on the floor.
QMacAutoReleasePool pool;
int stackSize = cocoaModalSessionStack.size();
for (int i=stackSize-1; i>=0; --i) {
QCocoaModalSessionInfo &info = cocoaModalSessionStack[i];
if (info.window) {
// This session has a window, and is therefore not marked
// as stopped. So just make it current. There might still be other
// stopped sessions on the stack, but those will be stopped on
// a later "cleanup" call.
currentModalSessionCached = info.session;
break;
}
currentModalSessionCached = nullptr;
if (info.session) {
Q_ASSERT(info.nswindow);
qCDebug(lcEventDispatcher) << "Ending modal session" << info.session
<< "for" << info.nswindow;
[NSApp endModalSession:info.session];
[(NSWindow *)info.nswindow release];
}
// remove the info now that we are finished with it
cocoaModalSessionStack.remove(i);
}
cleanupModalSessionsNeeded = false;
}
void QCocoaEventDispatcherPrivate::beginModalSession(QWindow *window)
{
qCDebug(lcEventDispatcher) << "Adding modal session for" << window;
if (std::any_of(cocoaModalSessionStack.constBegin(), cocoaModalSessionStack.constEnd(),
[&](const auto &sessionInfo) { return sessionInfo.window == window; })) {
qCWarning(lcEventDispatcher) << "Modal session for" << window << "already exists!";
return;
}
// We need to start spinning the modal session. Usually this is done with
// QDialog::exec() for Qt Widgets based applications, but for others that
// just call show(), we need to interrupt().
Q_Q(QCocoaEventDispatcher);
q->interrupt();
// Add a new, empty (null), NSModalSession to the stack.
// It will become active the next time QEventDispatcher::processEvents is called.
// A QCocoaModalSessionInfo is considered pending to become active if the window pointer
// is non-zero, and the session pointer is zero (it will become active upon a call to
// currentModalSession). A QCocoaModalSessionInfo is considered pending to be stopped if
// the window pointer is zero, and the session pointer is non-zero (it will be fully
// stopped in cleanupModalSessions()).
QCocoaModalSessionInfo info = {window, nullptr, nullptr};
cocoaModalSessionStack.push(info);
currentModalSessionCached = nullptr;
}
void QCocoaEventDispatcherPrivate::endModalSession(QWindow *window)
{
qCDebug(lcEventDispatcher) << "Removing modal session for" << window;
Q_Q(QCocoaEventDispatcher);
// Mark all sessions attached to window as pending to be stopped. We do this
// by setting the window pointer to zero, but leave the session pointer.
// We don't tell cocoa to stop any sessions just yet, because cocoa only understands
// when we stop the _current_ modal session (which is the session on top of
// the stack, and might not belong to 'window').
int stackSize = cocoaModalSessionStack.size();
int endedSessions = 0;
for (int i=stackSize-1; i>=0; --i) {
QCocoaModalSessionInfo &info = cocoaModalSessionStack[i];
if (!info.window)
endedSessions++;
if (info.window == window) {
info.window = nullptr;
if (i + endedSessions == stackSize-1) {
// The top sessions ended. Interrupt the event dispatcher to
// start spinning the correct session immediately.
q->interrupt();
currentModalSessionCached = nullptr;
cleanupModalSessionsNeeded = true;
}
}
}
}
QCocoaEventDispatcherPrivate::QCocoaEventDispatcherPrivate()
: processEventsFlags(0),
runLoopTimerRef(nullptr),
blockSendPostedEvents(false),
currentExecIsNSAppRun(false),
nsAppRunCalledByQt(false),
cleanupModalSessionsNeeded(false),
processEventsCalled(0),
currentModalSessionCached(nullptr),
lastSerial(-1),
interrupt(false)
{
}
QCocoaEventDispatcherPrivate::~QCocoaEventDispatcherPrivate()
= default;
void qt_mac_maybeCancelWaitForMoreEventsForwarder(QAbstractEventDispatcher *eventDispatcher)
{
static_cast<QCocoaEventDispatcher *>(eventDispatcher)->d_func()->maybeCancelWaitForMoreEvents();
}
QCocoaEventDispatcher::QCocoaEventDispatcher(QObject *parent)
: QAbstractEventDispatcherV2(*new QCocoaEventDispatcherPrivate, parent)
{
Q_D(QCocoaEventDispatcher);
d->cfSocketNotifier.setHostEventDispatcher(this);
d->cfSocketNotifier.setMaybeCancelWaitForMoreEventsCallback(qt_mac_maybeCancelWaitForMoreEventsForwarder);
// keep our sources running when modal loops are running
CFRunLoopAddCommonMode(mainRunLoop(), (CFStringRef) NSModalPanelRunLoopMode);
CFRunLoopSourceContext context;
bzero(&context, sizeof(CFRunLoopSourceContext));
context.info = d;
context.equal = runLoopSourceEqualCallback;
// source used to activate timers
context.perform = QCocoaEventDispatcherPrivate::activateTimersSourceCallback;
d->activateTimersSourceRef = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
Q_ASSERT(d->activateTimersSourceRef);
CFRunLoopAddSource(mainRunLoop(), d->activateTimersSourceRef, kCFRunLoopCommonModes);
// source used to send posted events
context.perform = QCocoaEventDispatcherPrivate::postedEventsSourceCallback;
d->postedEventsSource = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
Q_ASSERT(d->postedEventsSource);
CFRunLoopAddSource(mainRunLoop(), d->postedEventsSource, kCFRunLoopCommonModes);
// observer to emit aboutToBlock() and awake()
CFRunLoopObserverContext observerContext;
bzero(&observerContext, sizeof(CFRunLoopObserverContext));
observerContext.info = this;
d->waitingObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopBeforeWaiting | kCFRunLoopAfterWaiting,
true, 0,
QCocoaEventDispatcherPrivate::waitingObserverCallback,
&observerContext);
CFRunLoopAddObserver(mainRunLoop(), d->waitingObserver, kCFRunLoopCommonModes);
}
void QCocoaEventDispatcherPrivate::waitingObserverCallback(CFRunLoopObserverRef,
CFRunLoopActivity activity, void *info)
{
if (activity == kCFRunLoopBeforeWaiting)
emit static_cast<QCocoaEventDispatcher*>(info)->aboutToBlock();
else
emit static_cast<QCocoaEventDispatcher*>(info)->awake();
}
bool QCocoaEventDispatcherPrivate::sendQueuedUserInputEvents()
{
Q_Q(QCocoaEventDispatcher);
if (processEventsFlags & QEventLoop::ExcludeUserInputEvents)
return false;
bool didSendEvent = false;
while (!queuedUserInputEvents.isEmpty()) {
NSEvent *event = static_cast<NSEvent *>(queuedUserInputEvents.takeFirst());
if (!q->filterNativeEvent("NSEvent", event, nullptr)) {
[NSApp sendEvent:event];
didSendEvent = true;
}
[event release];
}
return didSendEvent;
}
void QCocoaEventDispatcherPrivate::processPostedEvents()
{
if (blockSendPostedEvents) {
// We're told to not send posted events (because the event dispatcher
// is currently working on setting up the correct session to run). But
// we still need to make sure that we don't fall asleep until pending events
// are sendt, so we just signal this need, and return:
CFRunLoopSourceSignal(postedEventsSource);
return;
}
if (cleanupModalSessionsNeeded && currentExecIsNSAppRun)
cleanupModalSessions();
if (processEventsCalled > 0 && interrupt) {
if (currentExecIsNSAppRun) {
// The event dispatcher has been interrupted. But since
// [NSApplication run] is running the event loop, we
// delayed stopping it until now (to let cocoa process
// pending cocoa events first).
if (currentModalSessionCached)
temporarilyStopAllModalSessions();
[NSApp stop:NSApp];
cancelWaitForMoreEvents();
}
return;
}
int serial = serialNumber.loadRelaxed();
if (!threadData.loadRelaxed()->canWait || (serial != lastSerial)) {
lastSerial = serial;
QCoreApplication::sendPostedEvents();
QWindowSystemInterface::sendWindowSystemEvents(QEventLoop::AllEvents);
}
}
void QCocoaEventDispatcherPrivate::postedEventsSourceCallback(void *info)
{
QCocoaEventDispatcherPrivate *d = static_cast<QCocoaEventDispatcherPrivate *>(info);
if (d->initializingNSApplication) {
qCDebug(lcEventDispatcher) << "Deferring" << __FUNCTION__ << "due to NSApp initialization";
// We don't want to process any sources during explicit NSApplication
// initialization, so defer the source until the actual event processing.
CFRunLoopSourceSignal(d->postedEventsSource);
return;
}
if (d->processEventsCalled && (d->processEventsFlags & QEventLoop::EventLoopExec) == 0) {
// processEvents() was called "manually," ignore this source for now
d->maybeCancelWaitForMoreEvents();
return;
}
d->sendQueuedUserInputEvents();
d->processPostedEvents();
d->maybeCancelWaitForMoreEvents();
}
void QCocoaEventDispatcherPrivate::cancelWaitForMoreEvents()
{
// In case the event dispatcher is waiting for more
// events somewhere, we post a dummy event to wake it up:
QMacAutoReleasePool pool;
[NSApp postEvent:[NSEvent otherEventWithType:NSEventTypeApplicationDefined location:NSZeroPoint
modifierFlags:0 timestamp:0. windowNumber:0 context:nil
subtype:QtCocoaEventSubTypeWakeup data1:0 data2:0] atStart:NO];
}
void QCocoaEventDispatcherPrivate::maybeCancelWaitForMoreEvents()
{
if ((processEventsFlags & (QEventLoop::EventLoopExec | QEventLoop::WaitForMoreEvents)) == QEventLoop::WaitForMoreEvents) {
// RunLoop sources are not NSEvents, but they do generate Qt events. If
// WaitForMoreEvents was set, but EventLoopExec is not, processEvents()
// should return after a source has sent some Qt events.
cancelWaitForMoreEvents();
}
}
void QCocoaEventDispatcher::interrupt()
{
Q_D(QCocoaEventDispatcher);
d->interrupt = true;
wakeUp();
// We do nothing more here than setting d->interrupt = true, and
// poke the event loop if it is sleeping. Actually stopping
// NSApp, or the current modal session, is done inside the send
// posted events callback. We do this to ensure that all current pending
// cocoa events gets delivered before we stop. Otherwise, if we now stop
// the last event loop recursion, cocoa will just drop pending posted
// events on the floor before we get a chance to reestablish a new session.
d->cancelWaitForMoreEvents();
}
// QTBUG-56746: The behavior of processEvents() has been changed to not clear
// the interrupt flag. Use this function to clear it.
void QCocoaEventDispatcher::clearCurrentThreadCocoaEventDispatcherInterruptFlag()
{
QCocoaEventDispatcher *cocoaEventDispatcher =
qobject_cast<QCocoaEventDispatcher *>(QThread::currentThread()->eventDispatcher());
if (!cocoaEventDispatcher)
return;
QCocoaEventDispatcherPrivate *cocoaEventDispatcherPrivate =
static_cast<QCocoaEventDispatcherPrivate *>(QObjectPrivate::get(cocoaEventDispatcher));
cocoaEventDispatcherPrivate->interrupt = false;
}
QCocoaEventDispatcher::~QCocoaEventDispatcher()
{
Q_D(QCocoaEventDispatcher);
d->timerInfoList.clearTimers();
d->maybeStopCFRunLoopTimer();
CFRunLoopRemoveSource(mainRunLoop(), d->activateTimersSourceRef, kCFRunLoopCommonModes);
CFRelease(d->activateTimersSourceRef);
// end all modal sessions
for (int i = 0; i < d->cocoaModalSessionStack.count(); ++i) {
QCocoaModalSessionInfo &info = d->cocoaModalSessionStack[i];
if (info.session) {
qCDebug(lcEventDispatcher) << "Ending modal session" << info.session
<< "for" << info.nswindow << "during shutdown";
[NSApp endModalSession:info.session];
[(NSWindow *)info.nswindow release];
}
}
// release all queued user input events
for (int i = 0; i < d->queuedUserInputEvents.count(); ++i) {
NSEvent *nsevent = static_cast<NSEvent *>(d->queuedUserInputEvents.at(i));
[nsevent release];
}
d->cfSocketNotifier.removeSocketNotifiers();
CFRunLoopRemoveSource(mainRunLoop(), d->postedEventsSource, kCFRunLoopCommonModes);
CFRelease(d->postedEventsSource);
CFRunLoopObserverInvalidate(d->waitingObserver);
CFRelease(d->waitingObserver);
}
QtCocoaInterruptDispatcher* QtCocoaInterruptDispatcher::instance = nullptr;
QtCocoaInterruptDispatcher::QtCocoaInterruptDispatcher() : cancelled(false)
{
// The whole point of this class is that we enable a way to interrupt
// the event dispatcher when returning back to a lower recursion level
// than where interruptLater was called. This is needed to detect if
// [NSApp run] should still be running at the recursion level it is at.
// Since the interrupt is canceled if processEvents is called before
// this object gets deleted, we also avoid interrupting unnecessary.
deleteLater();
}
QtCocoaInterruptDispatcher::~QtCocoaInterruptDispatcher()
{
if (cancelled)
return;
instance = nullptr;
QCocoaEventDispatcher::instance()->interrupt();
}
void QtCocoaInterruptDispatcher::cancelInterruptLater()
{
if (!instance)
return;
instance->cancelled = true;
delete instance;
instance = nullptr;
}
void QtCocoaInterruptDispatcher::interruptLater()
{
cancelInterruptLater();
instance = new QtCocoaInterruptDispatcher;
}
QT_END_NAMESPACE