diff options
Diffstat (limited to 'src/plugins/platforms/cocoa')
28 files changed, 418 insertions, 258 deletions
diff --git a/src/plugins/platforms/cocoa/qcocoaaccessibility.mm b/src/plugins/platforms/cocoa/qcocoaaccessibility.mm index 106c226adc..48edc4b5a6 100644 --- a/src/plugins/platforms/cocoa/qcocoaaccessibility.mm +++ b/src/plugins/platforms/cocoa/qcocoaaccessibility.mm @@ -386,6 +386,8 @@ id getValueAttribute(QAccessibleInterface *interface) } if (interface->state().checkable) { + if (interface->state().checkStateMixed) + return @(2); return interface->state().checked ? @(1) : @(0); } diff --git a/src/plugins/platforms/cocoa/qcocoaaccessibilityelement.mm b/src/plugins/platforms/cocoa/qcocoaaccessibilityelement.mm index ad40c6b0cb..e1f664c9da 100644 --- a/src/plugins/platforms/cocoa/qcocoaaccessibilityelement.mm +++ b/src/plugins/platforms/cocoa/qcocoaaccessibilityelement.mm @@ -246,7 +246,7 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of return iface->text(QAccessible::Name).toNSString(); } -- (BOOL) accessibilityEnabledAttribute { +- (BOOL) isAccessibilityEnabled { QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); if (!iface || !iface->isValid()) return false; diff --git a/src/plugins/platforms/cocoa/qcocoacolordialoghelper.mm b/src/plugins/platforms/cocoa/qcocoacolordialoghelper.mm index 5ad1f9d7bb..a57a38773f 100644 --- a/src/plugins/platforms/cocoa/qcocoacolordialoghelper.mm +++ b/src/plugins/platforms/cocoa/qcocoacolordialoghelper.mm @@ -324,9 +324,9 @@ public: bool show(Qt::WindowModality windowModality, QWindow *parent) { Q_UNUSED(parent); - if (windowModality != Qt::WindowModal) + if (windowModality != Qt::ApplicationModal) [mDelegate showModelessPanel]; - // no need to show a Qt::WindowModal dialog here, because it's necessary to call exec() in that case + // no need to show a Qt::ApplicationModal dialog here, because it will be shown in runApplicationModalPanel return true; } @@ -390,9 +390,8 @@ void QCocoaColorDialogHelper::exec() bool QCocoaColorDialogHelper::show(Qt::WindowFlags, Qt::WindowModality windowModality, QWindow *parent) { - if (windowModality == Qt::WindowModal) - windowModality = Qt::ApplicationModal; - + if (windowModality == Qt::ApplicationModal) + windowModality = Qt::WindowModal; // Workaround for Apple rdar://25792119: If you invoke // -setShowsAlpha: multiple times before showing the color // picker, its height grows irrevocably. Instead, only diff --git a/src/plugins/platforms/cocoa/qcocoacursor.mm b/src/plugins/platforms/cocoa/qcocoacursor.mm index 8ca72ec619..c963ff937c 100644 --- a/src/plugins/platforms/cocoa/qcocoacursor.mm +++ b/src/plugins/platforms/cocoa/qcocoacursor.mm @@ -45,6 +45,15 @@ #include <QtGui/QBitmap> +#if !defined(QT_APPLE_NO_PRIVATE_APIS) +@interface NSCursor() ++ (id)_windowResizeNorthWestSouthEastCursor; ++ (id)_windowResizeNorthEastSouthWestCursor; ++ (id)_windowResizeNorthSouthCursor; ++ (id)_windowResizeEastWestCursor; +@end +#endif // QT_APPLE_NO_PRIVATE_APIS + QT_BEGIN_NAMESPACE QCocoaCursor::QCocoaCursor() @@ -116,7 +125,7 @@ NSCursor *QCocoaCursor::convertCursor(QCursor *cursor) return nil; const Qt::CursorShape newShape = cursor->shape(); - NSCursor *cocoaCursor; + NSCursor *cocoaCursor = nil; // Check for a suitable built-in NSCursor first: switch (newShape) { @@ -157,7 +166,29 @@ NSCursor *QCocoaCursor::convertCursor(QCursor *cursor) case Qt::DragLinkCursor: cocoaCursor = [NSCursor dragLinkCursor]; break; - default : { +#if !defined(QT_APPLE_NO_PRIVATE_APIS) + case Qt::SizeVerCursor: + if ([NSCursor respondsToSelector:@selector(_windowResizeNorthSouthCursor)]) + cocoaCursor = [NSCursor _windowResizeNorthSouthCursor]; + break; + case Qt::SizeHorCursor: + if ([NSCursor respondsToSelector:@selector(_windowResizeEastWestCursor)]) + cocoaCursor = [NSCursor _windowResizeEastWestCursor]; + break; + case Qt::SizeBDiagCursor: + if ([NSCursor respondsToSelector:@selector(_windowResizeNorthEastSouthWestCursor)]) + cocoaCursor = [NSCursor _windowResizeNorthEastSouthWestCursor]; + break; + case Qt::SizeFDiagCursor: + if ([NSCursor respondsToSelector:@selector(_windowResizeNorthWestSouthEastCursor)]) + cocoaCursor = [NSCursor _windowResizeNorthWestSouthEastCursor]; + break; +#endif // QT_APPLE_NO_PRIVATE_APIS + default: + break; + } + + if (!cocoaCursor) { // No suitable OS cursor exist, use cursors provided // by Qt for the rest. Check for a cached cursor: cocoaCursor = m_cursors.value(newShape); @@ -172,8 +203,6 @@ NSCursor *QCocoaCursor::convertCursor(QCursor *cursor) m_cursors.insert(newShape, cocoaCursor); } - - break; } } return cocoaCursor; } diff --git a/src/plugins/platforms/cocoa/qcocoadrag.mm b/src/plugins/platforms/cocoa/qcocoadrag.mm index 4bd1b129bd..2af9b8f556 100644 --- a/src/plugins/platforms/cocoa/qcocoadrag.mm +++ b/src/plugins/platforms/cocoa/qcocoadrag.mm @@ -221,6 +221,12 @@ bool QCocoaDrag::maybeDragMultipleItems() // contains a combined picture for all urls we drag. auto imageOrNil = dragImage; for (const auto &qtUrl : qtUrls) { + if (!qtUrl.isValid()) + continue; + + if (qtUrl.isRelative()) // NSPasteboardWriting rejects such items. + continue; + NSURL *nsUrl = qtUrl.toNSURL(); auto *newItem = [[[NSDraggingItem alloc] initWithPasteboardWriter:nsUrl] autorelease]; const NSRect itemFrame = NSMakeRect(itemLocation.x, itemLocation.y, diff --git a/src/plugins/platforms/cocoa/qcocoafiledialoghelper.h b/src/plugins/platforms/cocoa/qcocoafiledialoghelper.h index dd0afbefe6..c9acbc8306 100644 --- a/src/plugins/platforms/cocoa/qcocoafiledialoghelper.h +++ b/src/plugins/platforms/cocoa/qcocoafiledialoghelper.h @@ -56,6 +56,7 @@ QT_NAMESPACE_ALIAS_OBJC_CLASS(QNSOpenSavePanelDelegate); QT_BEGIN_NAMESPACE +class QEventLoop; class QFileDialog; class QFileDialogPrivate; @@ -92,6 +93,7 @@ public: private: QNSOpenSavePanelDelegate *mDelegate; QUrl mDir; + QEventLoop *m_eventLoop = nullptr; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/cocoa/qcocoafiledialoghelper.mm b/src/plugins/platforms/cocoa/qcocoafiledialoghelper.mm index 15e83db48f..7c3f69b408 100644 --- a/src/plugins/platforms/cocoa/qcocoafiledialoghelper.mm +++ b/src/plugins/platforms/cocoa/qcocoafiledialoghelper.mm @@ -179,30 +179,31 @@ static QString strippedText(QString s) - (void)closePanel { *mCurrentSelection = QString::fromNSString([[mSavePanel URL] path]).normalized(QString::NormalizationForm_C); - if ([mSavePanel respondsToSelector:@selector(close)]) + + if (mSavePanel.sheet) + [NSApp endSheet:mSavePanel]; + else if (NSApp.modalWindow == mSavePanel) + [NSApp stopModal]; + else [mSavePanel close]; - if ([mSavePanel isSheet]) - [NSApp endSheet: mSavePanel]; } - (void)showModelessPanel { - if (mOpenPanel){ - QFileInfo info(*mCurrentSelection); - NSString *filepath = info.filePath().toNSString(); - NSURL *url = [NSURL fileURLWithPath:filepath isDirectory:info.isDir()]; - bool selectable = (mOptions->acceptMode() == QFileDialogOptions::AcceptSave) - || [self panel:mOpenPanel shouldEnableURL:url]; - - [self updateProperties]; - [mSavePanel setNameFieldStringValue:selectable ? info.fileName().toNSString() : @""]; - - [mOpenPanel beginWithCompletionHandler:^(NSInteger result){ - mReturnCode = result; - if (mHelper) - mHelper->QNSOpenSavePanelDelegate_panelClosed(result == NSModalResponseOK); - }]; - } + QFileInfo info(*mCurrentSelection); + NSString *filepath = info.filePath().toNSString(); + NSURL *url = [NSURL fileURLWithPath:filepath isDirectory:info.isDir()]; + bool selectable = (mOptions->acceptMode() == QFileDialogOptions::AcceptSave) + || [self panel:mSavePanel shouldEnableURL:url]; + + [self updateProperties]; + [mSavePanel setNameFieldStringValue:selectable ? info.fileName().toNSString() : @""]; + + [mSavePanel beginWithCompletionHandler:^(NSInteger result){ + mReturnCode = result; + if (mHelper) + mHelper->QNSOpenSavePanelDelegate_panelClosed(result == NSModalResponseOK); + }]; } - (BOOL)runApplicationModalPanel @@ -291,7 +292,12 @@ static QString strippedText(QString s) } } - QString qtFileName = QFileInfo(QString::fromNSString(filename)).fileName(); + // Treat symbolic links and aliases to directories like directories + QFileInfo fileInfo(QString::fromNSString(filename)); + if (fileInfo.isSymLink() && QFileInfo(fileInfo.symLinkTarget()).isDir()) + return YES; + + QString qtFileName = fileInfo.fileName(); // No filter means accept everything bool nameMatches = mSelectedNameFilter->isEmpty(); // Check if the current file name filter accepts the file: @@ -326,14 +332,7 @@ static QString strippedText(QString s) - (NSString *)panel:(id)sender userEnteredFilename:(NSString *)filename confirmed:(BOOL)okFlag { Q_UNUSED(sender); - if (!okFlag) - return filename; - if (!mOptions->testOption(QFileDialogOptions::DontConfirmOverwrite)) - return filename; - - // User has clicked save, and no overwrite confirmation should occur. - // To get the latter, we need to change the name we return (hence the prefix): - return [@"___qt_very_unlikely_prefix_" stringByAppendingString:filename]; + return filename; } - (void)setNameFilters:(const QStringList &)filters hideDetails:(BOOL)hideDetails @@ -391,7 +390,7 @@ static QString strippedText(QString s) if (fileInfo.suffix().isEmpty() && !defaultSuffix.isEmpty()) { filename.append('.').append(defaultSuffix); } - result << QUrl::fromLocalFile(filename.remove(QLatin1String("___qt_very_unlikely_prefix_"))); + result << QUrl::fromLocalFile(filename); return result; } } @@ -724,11 +723,14 @@ bool QCocoaFileDialogHelper::showCocoaFilePanel(Qt::WindowModality windowModalit createNSOpenSavePanelDelegate(); if (!mDelegate) return false; - if (windowModality == Qt::NonModal) - [mDelegate showModelessPanel]; - else if (windowModality == Qt::WindowModal && parent) + + if (windowModality == Qt::WindowModal && parent) [mDelegate showWindowModalSheet:parent]; - // no need to show a Qt::ApplicationModal dialog here, since it will be done in _q_platformRunNativeAppModalPanel() + else if (windowModality == Qt::ApplicationModal) + return true; // Defer until exec() + else + [mDelegate showModelessPanel]; + return true; } @@ -740,6 +742,10 @@ bool QCocoaFileDialogHelper::hideCocoaFilePanel() return false; } else { [mDelegate closePanel]; + + if (m_eventLoop) + m_eventLoop->exit(); + // Even when we hide it, we are still using a // native dialog, so return true: return true; @@ -748,16 +754,28 @@ bool QCocoaFileDialogHelper::hideCocoaFilePanel() void QCocoaFileDialogHelper::exec() { - // Note: If NSApp is not running (which is the case if e.g a top-most - // QEventLoop has been interrupted, and the second-most event loop has not - // yet been reactivated (regardless if [NSApp run] is still on the stack)), - // showing a native modal dialog will fail. - QMacAutoReleasePool pool; - if ([mDelegate runApplicationModalPanel]) - emit accept(); - else - emit reject(); + Q_ASSERT(mDelegate); + if (mDelegate->mSavePanel.visible) { + // WindowModal or NonModal, so already shown above + QEventLoop eventLoop; + m_eventLoop = &eventLoop; + eventLoop.exec(QEventLoop::DialogExec); + m_eventLoop = nullptr; + } else { + // ApplicationModal, so show and block using native APIs + + // Note: If NSApp is not running (which is the case if e.g a top-most + // QEventLoop has been interrupted, and the second-most event loop has not + // yet been reactivated (regardless if [NSApp run] is still on the stack)), + // showing a native modal dialog will fail. + + QMacAutoReleasePool pool; + if ([mDelegate runApplicationModalPanel]) + emit accept(); + else + emit reject(); + } } bool QCocoaFileDialogHelper::defaultNameFilterDisables() const diff --git a/src/plugins/platforms/cocoa/qcocoafontdialoghelper.mm b/src/plugins/platforms/cocoa/qcocoafontdialoghelper.mm index 7748c304e3..7b13259d55 100644 --- a/src/plugins/platforms/cocoa/qcocoafontdialoghelper.mm +++ b/src/plugins/platforms/cocoa/qcocoafontdialoghelper.mm @@ -314,9 +314,9 @@ public: bool show(Qt::WindowModality windowModality, QWindow *parent) { Q_UNUSED(parent); - if (windowModality != Qt::WindowModal) + if (windowModality != Qt::ApplicationModal) [mDelegate showModelessPanel]; - // no need to show a Qt::WindowModal dialog here, because it's necessary to call exec() in that case + // no need to show a Qt::ApplicationModal dialog here, because it will be shown in runApplicationModalPanel return true; } @@ -380,8 +380,8 @@ void QCocoaFontDialogHelper::exec() bool QCocoaFontDialogHelper::show(Qt::WindowFlags, Qt::WindowModality windowModality, QWindow *parent) { - if (windowModality == Qt::WindowModal) - windowModality = Qt::ApplicationModal; + if (windowModality == Qt::ApplicationModal) + windowModality = Qt::WindowModal; sharedFontPanel()->init(this); return sharedFontPanel()->show(windowModality, parent); } diff --git a/src/plugins/platforms/cocoa/qcocoamenu.mm b/src/plugins/platforms/cocoa/qcocoamenu.mm index 90d5180fed..68af82b5cd 100644 --- a/src/plugins/platforms/cocoa/qcocoamenu.mm +++ b/src/plugins/platforms/cocoa/qcocoamenu.mm @@ -290,27 +290,26 @@ void QCocoaMenu::syncSeparatorsCollapsible(bool enable) QMacAutoReleasePool pool; if (enable) { bool previousIsSeparator = true; // setting to true kills all the separators placed at the top. - NSMenuItem *previousItem = nil; + NSMenuItem *lastVisibleItem = nil; for (NSMenuItem *item in m_nativeMenu.itemArray) { if (item.separatorItem) { + // hide item if previous was a separator, or if it's explicitly hidden + bool hideItem = previousIsSeparator; if (auto *cocoaItem = qt_objc_cast<QCocoaNSMenuItem *>(item).platformMenuItem) - cocoaItem->setVisible(!previousIsSeparator); - item.hidden = previousIsSeparator; + hideItem = previousIsSeparator || !cocoaItem->isVisible(); + item.hidden = hideItem; } if (!item.hidden) { - previousItem = item; - previousIsSeparator = previousItem.separatorItem; + lastVisibleItem = item; + previousIsSeparator = lastVisibleItem.separatorItem; } } // We now need to check the final item since we don't want any separators at the end of the list. - if (previousItem && previousIsSeparator) { - if (auto *cocoaItem = qt_objc_cast<QCocoaNSMenuItem *>(previousItem).platformMenuItem) - cocoaItem->setVisible(false); - previousItem.hidden = YES; - } + if (lastVisibleItem && lastVisibleItem.separatorItem) + lastVisibleItem.hidden = YES; } else { for (auto *item : qAsConst(m_menuItems)) { if (!item->isSeparator()) @@ -351,6 +350,17 @@ void QCocoaMenu::showPopup(const QWindow *parentWindow, const QRect &targetRect, NSView *view = cocoaWindow ? cocoaWindow->view() : nil; NSMenuItem *nsItem = item ? ((QCocoaMenuItem *)item)->nsItem() : nil; + // store the window that this popup belongs to so that we can evaluate whether we are modally blocked + bool resetMenuParent = false; + if (!menuParent()) { + setMenuParent(cocoaWindow); + resetMenuParent = true; + } + auto menuParentGuard = qScopeGuard([&]{ + if (resetMenuParent) + setMenuParent(nullptr); + }); + QScreen *screen = nullptr; if (parentWindow) screen = parentWindow->screen(); @@ -377,7 +387,7 @@ void QCocoaMenu::showPopup(const QWindow *parentWindow, const QRect &targetRect, QCocoaScreen *cocoaScreen = static_cast<QCocoaScreen *>(screen->handle()); int availableHeight = cocoaScreen->availableGeometry().height(); - const QPoint &globalPos = cocoaWindow->mapToGlobal(pos); + const QPoint globalPos = cocoaWindow ? cocoaWindow->mapToGlobal(pos) : pos; int menuHeight = m_nativeMenu.size.height; if (globalPos.y() + menuHeight > availableHeight) { // Maybe we need to fix the vertical popup position but we don't know the @@ -421,7 +431,7 @@ void QCocoaMenu::showPopup(const QWindow *parentWindow, const QRect &targetRect, // The calls above block, and also swallow any mouse release event, // so we need to clear any mouse button that triggered the menu popup. - if (!cocoaWindow->isForeignWindow()) + if (cocoaWindow && !cocoaWindow->isForeignWindow()) [qnsview_cast(view) resetMouseButtons]; } diff --git a/src/plugins/platforms/cocoa/qcocoamenuitem.h b/src/plugins/platforms/cocoa/qcocoamenuitem.h index 029d29be9d..e6bbf14367 100644 --- a/src/plugins/platforms/cocoa/qcocoamenuitem.h +++ b/src/plugins/platforms/cocoa/qcocoamenuitem.h @@ -117,6 +117,7 @@ public: inline bool isMerged() const { return m_merged; } inline bool isEnabled() const { return m_enabled && m_parentEnabled; } inline bool isSeparator() const { return m_isSeparator; } + inline bool isVisible() const { return m_isVisible; } QCocoaMenu *menu() const { return m_menu; } MenuRole effectiveRole() const; diff --git a/src/plugins/platforms/cocoa/qcocoamenuitem.mm b/src/plugins/platforms/cocoa/qcocoamenuitem.mm index 3b37e7c9c1..258aee82a5 100644 --- a/src/plugins/platforms/cocoa/qcocoamenuitem.mm +++ b/src/plugins/platforms/cocoa/qcocoamenuitem.mm @@ -300,8 +300,7 @@ NSMenuItem *QCocoaMenuItem::sync() while (depth < 3 && p && !(menubar = qobject_cast<QCocoaMenuBar *>(p))) { ++depth; QCocoaMenuObject *menuObject = dynamic_cast<QCocoaMenuObject *>(p); - Q_ASSERT(menuObject); - p = menuObject->menuParent(); + p = menuObject ? menuObject->menuParent() : nullptr; } if (menubar && depth < 3) diff --git a/src/plugins/platforms/cocoa/qcocoamenuloader.mm b/src/plugins/platforms/cocoa/qcocoamenuloader.mm index a7c17fc177..5f7c361a3d 100644 --- a/src/plugins/platforms/cocoa/qcocoamenuloader.mm +++ b/src/plugins/platforms/cocoa/qcocoamenuloader.mm @@ -322,7 +322,8 @@ return [NSApp validateMenuItem:menuItem]; if (menuItem.action == @selector(hide:)) { - if (QCocoaIntegration::instance()->activePopupWindow()) + auto *w = QCocoaIntegration::instance()->activePopupWindow(); + if (w && (w->window()->type() != Qt::ToolTip)) return NO; return [NSApp validateMenuItem:menuItem]; } diff --git a/src/plugins/platforms/cocoa/qcocoascreen.h b/src/plugins/platforms/cocoa/qcocoascreen.h index dcf6f1c753..448b281665 100644 --- a/src/plugins/platforms/cocoa/qcocoascreen.h +++ b/src/plugins/platforms/cocoa/qcocoascreen.h @@ -45,6 +45,7 @@ #include "qcocoacursor.h" #include <qpa/qplatformintegration.h> +#include <QtCore/private/qcore_mac_p.h> QT_BEGIN_NAMESPACE @@ -64,8 +65,8 @@ public: QImage::Format format() const override { return m_format; } qreal devicePixelRatio() const override { return m_devicePixelRatio; } QSizeF physicalSize() const override { return m_physicalSize; } - QDpi logicalDpi() const override { return m_logicalDpi; } - QDpi logicalBaseDpi() const override { return m_logicalDpi; } + QDpi logicalDpi() const override { return QDpi(72, 72); } + QDpi logicalBaseDpi() const override { return QDpi(72, 72); } qreal refreshRate() const override { return m_refreshRate; } QString name() const override { return m_name; } QPlatformCursor *cursor() const override { return m_cursor; } @@ -96,8 +97,8 @@ private: static void updateScreens(); static void cleanupScreens(); - static bool updateScreensIfNeeded(); - static NSArray *s_screenConfigurationBeforeUpdate; + static QMacNotificationObserver s_screenParameterObserver; + static CGDisplayReconfigurationCallBack s_displayReconfigurationCallBack; static void add(CGDirectDisplayID displayId); QCocoaScreen(CGDirectDisplayID displayId); @@ -112,7 +113,6 @@ private: QRect m_geometry; QRect m_availableGeometry; - QDpi m_logicalDpi; qreal m_refreshRate = 0; int m_depth = 0; QString m_name; diff --git a/src/plugins/platforms/cocoa/qcocoascreen.mm b/src/plugins/platforms/cocoa/qcocoascreen.mm index 6a3172fb19..203df61d82 100644 --- a/src/plugins/platforms/cocoa/qcocoascreen.mm +++ b/src/plugins/platforms/cocoa/qcocoascreen.mm @@ -72,91 +72,33 @@ namespace CoreGraphics { Q_ENUM_NS(DisplayChange) } -NSArray *QCocoaScreen::s_screenConfigurationBeforeUpdate = nil; +QMacNotificationObserver QCocoaScreen::s_screenParameterObserver; +CGDisplayReconfigurationCallBack QCocoaScreen::s_displayReconfigurationCallBack = nullptr; void QCocoaScreen::initializeScreens() { updateScreens(); - CGDisplayRegisterReconfigurationCallback([](CGDirectDisplayID displayId, CGDisplayChangeSummaryFlags flags, void *userInfo) { + s_displayReconfigurationCallBack = [](CGDirectDisplayID displayId, CGDisplayChangeSummaryFlags flags, void *userInfo) { Q_UNUSED(userInfo); - // Displays are reconfigured in batches, and we want to update our screens - // once a batch ends, so that all the states of the displays are up to date. - static int displayReconfigurationsInProgress = 0; - const bool beforeReconfigure = flags & kCGDisplayBeginConfigurationFlag; - qCDebug(lcQpaScreen).verbosity(0).nospace() << "Display " << displayId - << (beforeReconfigure ? " about to reconfigure" : " was ") - << QFlags<CoreGraphics::DisplayChange>(flags) - << " with " << displayReconfigurationsInProgress - << " display configuration(s) in progress"; - - if (!flags) { - // CGDisplayRegisterReconfigurationCallback has been observed to be called - // with flags unset. This seems like a bug. The callback is not paired with - // a matching "completion" callback either, so we don't know whether to treat - // it as a begin or end of reconfigure. - return; - } - - if (beforeReconfigure) { - if (!displayReconfigurationsInProgress++) { - // There might have been a screen reconfigure before this that - // we didn't process yet, so do that now if that's the case. - updateScreensIfNeeded(); - - Q_ASSERT(!s_screenConfigurationBeforeUpdate); - s_screenConfigurationBeforeUpdate = NSScreen.screens; - qCDebug(lcQpaScreen, "Display reconfigure transaction started" - " with screen configuration %p", s_screenConfigurationBeforeUpdate); - - static void (^tryScreenUpdate)(); - tryScreenUpdate = ^void () { - qCDebug(lcQpaScreen) << "Attempting screen update from runloop block"; - if (!updateScreensIfNeeded()) - CFRunLoopPerformBlock(CFRunLoopGetMain(), kCFRunLoopCommonModes, tryScreenUpdate); - }; - CFRunLoopPerformBlock(CFRunLoopGetMain(), kCFRunLoopCommonModes, tryScreenUpdate); - } - } else { - Q_ASSERT_X(displayReconfigurationsInProgress, "QCococaScreen", - "Display configuration transactions are expected to be balanced"); + qCDebug(lcQpaScreen).verbosity(0) << "Display" << displayId + << (beforeReconfigure ? "beginning" : "finished") << "reconfigure" + << QFlags<CoreGraphics::DisplayChange>(flags); - if (!--displayReconfigurationsInProgress) { - qCDebug(lcQpaScreen) << "Display reconfigure transaction completed"; - // We optimistically update now, in case the NSScreens have changed - updateScreensIfNeeded(); - } - } - }, nullptr); + if (!beforeReconfigure) + updateScreens(); + }; + CGDisplayRegisterReconfigurationCallback(s_displayReconfigurationCallBack, nullptr); - static QMacNotificationObserver screenParameterObserver(NSApplication.sharedApplication, + s_screenParameterObserver = QMacNotificationObserver(NSApplication.sharedApplication, NSApplicationDidChangeScreenParametersNotification, [&]() { qCDebug(lcQpaScreen) << "Received screen parameter change notification"; - updateScreensIfNeeded(); // As a last resort we update screens here + updateScreens(); }); } -bool QCocoaScreen::updateScreensIfNeeded() -{ - if (!s_screenConfigurationBeforeUpdate) { - qCDebug(lcQpaScreen) << "QScreens have already been updated, all good"; - return true; - } - - if (s_screenConfigurationBeforeUpdate == NSScreen.screens) { - qCDebug(lcQpaScreen) << "Still waiting for NSScreen configuration change"; - return false; - } - - qCDebug(lcQpaScreen, "NSScreen configuration changed to %p", NSScreen.screens); - updateScreens(); - - s_screenConfigurationBeforeUpdate = nil; - return true; -} - /* Update the list of available QScreens, and the properties of existing screens. @@ -164,6 +106,18 @@ bool QCocoaScreen::updateScreensIfNeeded() */ void QCocoaScreen::updateScreens() { + // Adding, updating, or removing a screen below might trigger + // Qt or the application to move a window to a different screen, + // recursing back here via QCocoaWindow::windowDidChangeScreen. + // The update code is not re-entrant, so bail out if we end up + // in this situation. The screens will stabilize eventually. + static bool updatingScreens = false; + if (updatingScreens) { + qCInfo(lcQpaScreen) << "Skipping screen update, already updating"; + return; + } + QBoolBlocker recursionGuard(updatingScreens); + uint32_t displayCount = 0; if (CGGetOnlineDisplayList(0, nullptr, &displayCount) != kCGErrorSuccess) qFatal("Failed to get number of online displays"); @@ -239,6 +193,12 @@ void QCocoaScreen::cleanupScreens() // Remove screens in reverse order to avoid crash in case of multiple screens for (QScreen *screen : backwards(QGuiApplication::screens())) static_cast<QCocoaScreen*>(screen->handle())->remove(); + + Q_ASSERT(s_displayReconfigurationCallBack); + CGDisplayRemoveReconfigurationCallback(s_displayReconfigurationCallBack, nullptr); + s_displayReconfigurationCallBack = nullptr; + + s_screenParameterObserver.remove(); } void QCocoaScreen::remove() @@ -282,13 +242,13 @@ static QString displayName(CGDirectDisplayID displayID) NSDictionary *info = [(__bridge NSDictionary*)IODisplayCreateInfoDictionary( display, kIODisplayOnlyPreferredName) autorelease]; - if ([[info objectForKey:@kDisplayVendorID] longValue] != CGDisplayVendorNumber(displayID)) + if ([[info objectForKey:@kDisplayVendorID] unsignedIntValue] != CGDisplayVendorNumber(displayID)) continue; - if ([[info objectForKey:@kDisplayProductID] longValue] != CGDisplayModelNumber(displayID)) + if ([[info objectForKey:@kDisplayProductID] unsignedIntValue] != CGDisplayModelNumber(displayID)) continue; - if ([[info objectForKey:@kDisplaySerialNumber] longValue] != CGDisplaySerialNumber(displayID)) + if ([[info objectForKey:@kDisplaySerialNumber] unsignedIntValue] != CGDisplaySerialNumber(displayID)) continue; NSDictionary *localizedNames = [info objectForKey:@kDisplayProductName]; @@ -310,15 +270,17 @@ void QCocoaScreen::update(CGDirectDisplayID displayId) Q_ASSERT(isOnline()); + // Some properties are only available via NSScreen + NSScreen *nsScreen = nativeScreen(); + if (!nsScreen) { + qCDebug(lcQpaScreen) << "Corresponding NSScreen not yet available. Deferring update"; + return; + } + const QRect previousGeometry = m_geometry; const QRect previousAvailableGeometry = m_availableGeometry; - const QDpi previousLogicalDpi = m_logicalDpi; const qreal previousRefreshRate = m_refreshRate; - // Some properties are only available via NSScreen - NSScreen *nsScreen = nativeScreen(); - Q_ASSERT(nsScreen); - // The reference screen for the geometry is always the primary screen QRectF primaryScreenGeometry = QRectF::fromCGRect(CGDisplayBounds(CGMainDisplayID())); m_geometry = qt_mac_flip(QRectF::fromCGRect(nsScreen.frame), primaryScreenGeometry).toRect(); @@ -331,8 +293,6 @@ void QCocoaScreen::update(CGDirectDisplayID displayId) CGSize size = CGDisplayScreenSize(m_displayId); m_physicalSize = QSizeF(size.width, size.height); - m_logicalDpi.first = 72; - m_logicalDpi.second = 72; QCFType<CGDisplayModeRef> displayMode = CGDisplayCopyDisplayMode(m_displayId); float refresh = CGDisplayModeGetRefreshRate(displayMode); @@ -344,8 +304,6 @@ void QCocoaScreen::update(CGDirectDisplayID displayId) if (didChangeGeometry) QWindowSystemInterface::handleScreenGeometryChange(screen(), geometry(), availableGeometry()); - if (m_logicalDpi != previousLogicalDpi) - QWindowSystemInterface::handleScreenLogicalDotsPerInchChange(screen(), m_logicalDpi.first, m_logicalDpi.second); if (m_refreshRate != previousRefreshRate) QWindowSystemInterface::handleScreenRefreshRateChange(screen(), m_refreshRate); } @@ -358,6 +316,11 @@ void QCocoaScreen::requestUpdate() { Q_ASSERT(m_displayId); + if (!isOnline()) { + qCDebug(lcQpaScreenUpdates) << this << "is not online. Ignoring update request"; + return; + } + if (!m_displayLink) { CVDisplayLinkCreateWithCGDisplay(m_displayId, &m_displayLink); CVDisplayLinkSetOutputCallback(m_displayLink, [](CVDisplayLinkRef, const CVTimeStamp*, @@ -674,8 +637,8 @@ bool QCocoaScreen::isOnline() const // returning -1 to signal that the displayId is invalid. Some functions // will also assert or even crash in this case, so it's important that // we double check if a display is online before calling other functions. - auto isOnline = CGDisplayIsOnline(m_displayId); - static const uint32_t kCGDisplayIsDisconnected = int32_t(-1); + int isOnline = CGDisplayIsOnline(m_displayId); + static const int kCGDisplayIsDisconnected = 0xffffffff; return isOnline != kCGDisplayIsDisconnected && isOnline; } @@ -714,13 +677,17 @@ QList<QPlatformScreen*> QCocoaScreen::virtualSiblings() const QCocoaScreen *QCocoaScreen::get(NSScreen *nsScreen) { - if (s_screenConfigurationBeforeUpdate) { - qCWarning(lcQpaScreen) << "Trying to resolve screen while waiting for screen reconfigure!"; - if (!updateScreensIfNeeded()) - qCWarning(lcQpaScreen) << "Failed to do last minute screen update. Expect crashes."; + auto displayId = nsScreen.qt_displayId; + auto *cocoaScreen = get(displayId); + if (!cocoaScreen) { + qCWarning(lcQpaScreen) << "Failed to map" << nsScreen + << "to QCocoaScreen. Doing last minute update."; + updateScreens(); + cocoaScreen = get(displayId); + if (!cocoaScreen) + qCWarning(lcQpaScreen) << "Last minute update failed!"; } - - return get(nsScreen.qt_displayId); + return cocoaScreen; } QCocoaScreen *QCocoaScreen::get(CGDirectDisplayID displayId) diff --git a/src/plugins/platforms/cocoa/qcocoasystemtrayicon.mm b/src/plugins/platforms/cocoa/qcocoasystemtrayicon.mm index 213df4eba7..f3b4ba98bb 100644 --- a/src/plugins/platforms/cocoa/qcocoasystemtrayicon.mm +++ b/src/plugins/platforms/cocoa/qcocoasystemtrayicon.mm @@ -101,7 +101,7 @@ void QCocoaSystemTrayIcon::init() m_statusItem.button.target = m_delegate; m_statusItem.button.action = @selector(statusItemClicked); - [m_statusItem.button sendActionOn:NSEventMaskLeftMouseUp | NSEventMaskRightMouseUp | NSEventMaskOtherMouseUp]; + [m_statusItem.button sendActionOn:NSEventMaskLeftMouseDown | NSEventMaskRightMouseDown | NSEventMaskOtherMouseDown]; } void QCocoaSystemTrayIcon::cleanup() @@ -203,6 +203,7 @@ void QCocoaSystemTrayIcon::updateIcon(const QIcon &icon) r.moveCenter(fullHeightPixmap.rect().center()); p.drawPixmap(r, pixmap); } + fullHeightPixmap.setDevicePixelRatio(devicePixelRatio); auto *nsimage = [NSImage imageFromQImage:fullHeightPixmap.toImage()]; [nsimage setTemplate:icon.isMask()]; diff --git a/src/plugins/platforms/cocoa/qcocoatheme.mm b/src/plugins/platforms/cocoa/qcocoatheme.mm index d73b028afb..b6ab9c0bbc 100644 --- a/src/plugins/platforms/cocoa/qcocoatheme.mm +++ b/src/plugins/platforms/cocoa/qcocoatheme.mm @@ -465,11 +465,11 @@ QPixmap QCocoaTheme::standardPixmap(StandardPixmap sp, const QSizeF &size) const if (iconType != 0) { QPixmap pixmap; IconRef icon = nullptr; - GetIconRef(kOnSystemDisk, kSystemIconsCreator, iconType, &icon); + QT_IGNORE_DEPRECATIONS(GetIconRef(kOnSystemDisk, kSystemIconsCreator, iconType, &icon)); if (icon) { pixmap = qt_mac_convert_iconref(icon, size.width(), size.height()); - ReleaseIconRef(icon); + QT_IGNORE_DEPRECATIONS(ReleaseIconRef(icon)); } return pixmap; diff --git a/src/plugins/platforms/cocoa/qcocoawindow.h b/src/plugins/platforms/cocoa/qcocoawindow.h index 4688598da7..1d0948a0ec 100644 --- a/src/plugins/platforms/cocoa/qcocoawindow.h +++ b/src/plugins/platforms/cocoa/qcocoawindow.h @@ -56,6 +56,8 @@ #include <MoltenVK/mvk_vulkan.h> #endif +#include <QHash> + QT_BEGIN_NAMESPACE #ifndef QT_NO_DEBUG_STREAM diff --git a/src/plugins/platforms/cocoa/qcocoawindow.mm b/src/plugins/platforms/cocoa/qcocoawindow.mm index ca67718e90..6bfdd82e19 100644 --- a/src/plugins/platforms/cocoa/qcocoawindow.mm +++ b/src/plugins/platforms/cocoa/qcocoawindow.mm @@ -104,7 +104,7 @@ static void qRegisterNotificationCallbacks() if (QNSView *qnsView = qnsview_cast(notification.object)) cocoaWindows += qnsView.platformWindow; } else { - qCWarning(lcCocoaNotifications) << "Unhandled notifcation" + qCWarning(lcCocoaNotifications) << "Unhandled notification" << notification.name << "for" << notification.object; return; } @@ -367,12 +367,18 @@ void QCocoaWindow::setVisible(bool visible) if (window()->windowState() != Qt::WindowMinimized) { if (parentCocoaWindow && (window()->modality() == Qt::WindowModal || window()->type() == Qt::Sheet)) { // Show the window as a sheet - [parentCocoaWindow->nativeWindow() beginSheet:m_view.window completionHandler:nil]; + NSWindow *nativeParentWindow = parentCocoaWindow->nativeWindow(); + if (!nativeParentWindow.attachedSheet) + [nativeParentWindow beginSheet:m_view.window completionHandler:nil]; + else + [nativeParentWindow beginCriticalSheet:m_view.window completionHandler:nil]; } else if (window()->modality() == Qt::ApplicationModal) { // Show the window as application modal eventDispatcher()->beginModalSession(window()); } else if (m_view.window.canBecomeKeyWindow) { - bool shouldBecomeKeyNow = !NSApp.modalWindow || m_view.window.worksWhenModal; + bool shouldBecomeKeyNow = !NSApp.modalWindow + || m_view.window.worksWhenModal + || !NSApp.modalWindow.visible; // Panels with becomesKeyOnlyIfNeeded set should not activate until a view // with needsPanelToBecomeKey, for example a line edit, is clicked. @@ -522,7 +528,10 @@ NSUInteger QCocoaWindow::windowStyleMask(Qt::WindowFlags flags) NSUInteger styleMask = (frameless || !resizable) ? NSWindowStyleMaskBorderless : NSWindowStyleMaskResizable; if (frameless) { - // No further customizations for frameless since there are no window decorations. + // Frameless windows do not display the traffic lights buttons for + // e.g. minimize, however StyleMaskMiniaturizable is required to allow + // programmatic minimize. + styleMask |= NSWindowStyleMaskMiniaturizable; } else if (flags & Qt::CustomizeWindowHint) { if (flags & Qt::WindowTitleHint) styleMask |= NSWindowStyleMaskTitled; @@ -545,9 +554,11 @@ NSUInteger QCocoaWindow::windowStyleMask(Qt::WindowFlags flags) if (m_drawContentBorderGradient) styleMask |= NSWindowStyleMaskTexturedBackground; - // Don't wipe fullscreen state + // Don't wipe existing states if (m_view.window.styleMask & NSWindowStyleMaskFullScreen) styleMask |= NSWindowStyleMaskFullScreen; + if (m_view.window.styleMask & NSWindowStyleMaskFullSizeContentView) + styleMask |= NSWindowStyleMaskFullSizeContentView; return styleMask; } @@ -676,9 +687,10 @@ void QCocoaWindow::applyWindowState(Qt::WindowStates requestedState) switch (currentState) { case Qt::WindowMinimized: [nsWindow deminiaturize:sender]; - Q_ASSERT_X(windowState() != Qt::WindowMinimized, "QCocoaWindow", - "[NSWindow deminiaturize:] is synchronous"); - break; + // Deminiaturizing is not synchronous, so we need to wait for the + // NSWindowDidMiniaturizeNotification before continuing to apply + // the new state. + return; case Qt::WindowFullScreen: { toggleFullScreen(); // Exiting fullscreen is not synchronous, so we need to wait for the @@ -842,7 +854,15 @@ void QCocoaWindow::windowDidDeminiaturize() if (!isContentView()) return; + Qt::WindowState requestedState = window()->windowState(); + handleWindowStateChanged(); + + if (requestedState != windowState() && requestedState != Qt::WindowMinimized) { + // We were only going out of minimized as an intermediate step before + // progressing into the final step, so re-sync the desired state. + applyWindowState(requestedState); + } } void QCocoaWindow::handleWindowStateChanged(HandleFlags flags) @@ -1265,11 +1285,15 @@ void QCocoaWindow::windowDidChangeScreen() return; // Note: When a window is resized to 0x0 Cocoa will report the window's screen as nil - auto *currentScreen = QCocoaScreen::get(m_view.window.screen); + NSScreen *nsScreen = m_view.window.screen; + + qCDebug(lcQpaWindow) << window() << "did change" << nsScreen; + QCocoaScreen::updateScreens(); + auto *previousScreen = static_cast<QCocoaScreen*>(screen()); + auto *currentScreen = QCocoaScreen::get(nsScreen); - Q_ASSERT_X(!m_view.window.screen || currentScreen, - "QCocoaWindow", "Failed to get QCocoaScreen for NSScreen"); + qCDebug(lcQpaWindow) << "Screen changed for" << window() << "from" << previousScreen << "to" << currentScreen; // Note: The previous screen may be the same as the current screen, either because // a) the screen was just reconfigured, which still results in AppKit sending an @@ -1282,7 +1306,6 @@ void QCocoaWindow::windowDidChangeScreen() // device-pixel ratio may have changed, and needs to be delivered to all // windows, both top level and child windows. - qCDebug(lcQpaWindow) << "Screen changed for" << window() << "from" << previousScreen << "to" << currentScreen; QWindowSystemInterface::handleWindowScreenChanged<QWindowSystemInterface::SynchronousDelivery>( window(), currentScreen ? currentScreen->screen() : nullptr); @@ -1307,10 +1330,19 @@ void QCocoaWindow::windowWillClose() bool QCocoaWindow::windowShouldClose() { qCDebug(lcQpaWindow) << "QCocoaWindow::windowShouldClose" << window(); + // This callback should technically only determine if the window // should (be allowed to) close, but since our QPA API to determine // that also involves actually closing the window we do both at the // same time, instead of doing the latter in windowWillClose. + + // If the window is closed, we will release and deallocate the NSWindow. + // But frames higher up in the stack might still expect the window to + // be alive, since the windowShouldClose: callback is technically only + // supposed to answer YES or NO. To ensure the window is still alive + // we put an autorelease in the closest pool (typically the runloop). + [[m_view.window retain] autorelease]; + return QWindowSystemInterface::handleCloseEvent<QWindowSystemInterface::SynchronousDelivery>(window()); } @@ -1459,11 +1491,6 @@ void QCocoaWindow::recreateWindowIfNeeded() if ((isContentView() && !shouldBeContentView) || (recreateReason & PanelChanged)) { if (m_nsWindow) { qCDebug(lcQpaWindow) << "Getting rid of existing window" << m_nsWindow; - if (m_nsWindow.observationInfo) { - qCCritical(lcQpaWindow) << m_nsWindow << "has active key-value observers (KVO)!" - << "These will stop working now that the window is recreated, and will result in exceptions" - << "when the observers are removed. Break in QCocoaWindow::recreateWindowIfNeeded to debug."; - } [m_nsWindow closeAndRelease]; if (isContentView() && !isEmbeddedView) { // We explicitly disassociate m_view from the window's contentView, @@ -1719,6 +1746,20 @@ void QCocoaWindow::setWindowCursor(NSCursor *cursor) view.cursor = cursor; [m_view.window invalidateCursorRectsForView:m_view]; + + // There's a bug in AppKit where calling invalidateCursorRectsForView when + // there's an override cursor active (for example when hovering over the + // window frame), will not result in a cursorUpdate: callback. To work around + // this we synthesize a cursor update event and call the callback ourselves, + // if we detect that the mouse is currently over the view. + auto locationInWindow = m_view.window.mouseLocationOutsideOfEventStream; + auto locationInSuperview = [m_view.superview convertPoint:locationInWindow fromView:nil]; + if ([m_view hitTest:locationInSuperview] == m_view) { + [m_view cursorUpdate:[NSEvent enterExitEventWithType:NSEventTypeCursorUpdate + location:locationInWindow modifierFlags:0 timestamp:0 + windowNumber:m_view.window.windowNumber context:nil + eventNumber:0 trackingNumber:0 userData:0]]; + } } void QCocoaWindow::registerTouch(bool enable) @@ -1871,7 +1912,7 @@ bool QCocoaWindow::shouldRefuseKeyWindowAndFirstResponder() // This function speaks up if there's any reason // to refuse key window or first responder state. - if (window()->flags() & Qt::WindowDoesNotAcceptFocus) + if (window()->flags() & (Qt::WindowDoesNotAcceptFocus | Qt::WindowTransparentForInput)) return true; if (m_inSetVisible) { diff --git a/src/plugins/platforms/cocoa/qiosurfacegraphicsbuffer.h b/src/plugins/platforms/cocoa/qiosurfacegraphicsbuffer.h index e070ba977d..0896917334 100644 --- a/src/plugins/platforms/cocoa/qiosurfacegraphicsbuffer.h +++ b/src/plugins/platforms/cocoa/qiosurfacegraphicsbuffer.h @@ -43,6 +43,8 @@ #include <qpa/qplatformgraphicsbuffer.h> #include <private/qcore_mac_p.h> +#include <CoreGraphics/CGColorSpace.h> + QT_BEGIN_NAMESPACE class QIOSurfaceGraphicsBuffer : public QPlatformGraphicsBuffer diff --git a/src/plugins/platforms/cocoa/qnsview.h b/src/plugins/platforms/cocoa/qnsview.h index 0a18afe3a6..1aa400af8d 100644 --- a/src/plugins/platforms/cocoa/qnsview.h +++ b/src/plugins/platforms/cocoa/qnsview.h @@ -61,6 +61,7 @@ QT_NAMESPACE_ALIAS_OBJC_CLASS(QNSView); @interface QNSView (MouseAPI) - (void)handleFrameStrutMouseEvent:(NSEvent *)theEvent; +- (bool)closePopups:(NSEvent *)theEvent; - (void)resetMouseButtons; @end diff --git a/src/plugins/platforms/cocoa/qnsview.mm b/src/plugins/platforms/cocoa/qnsview.mm index a6e5ca5f7b..e0f7e4755b 100644 --- a/src/plugins/platforms/cocoa/qnsview.mm +++ b/src/plugins/platforms/cocoa/qnsview.mm @@ -113,6 +113,7 @@ QT_NAMESPACE_ALIAS_OBJC_CLASS(QNSViewMouseMoveHelper); @interface QNSView (ComplexText) <NSTextInputClient> - (void)textInputContextKeyboardSelectionDidChangeNotification:(NSNotification *)textInputContextKeyboardSelectionDidChangeNotification; +@property (readonly) QObject* focusObject; @end @implementation QNSView { diff --git a/src/plugins/platforms/cocoa/qnsview_complextext.mm b/src/plugins/platforms/cocoa/qnsview_complextext.mm index 5926840cf3..28f6817a51 100644 --- a/src/plugins/platforms/cocoa/qnsview_complextext.mm +++ b/src/plugins/platforms/cocoa/qnsview_complextext.mm @@ -63,7 +63,7 @@ - (void)unmarkText { if (!m_composingText.isEmpty()) { - if (QObject *fo = m_platformWindow->window()->focusObject()) { + if (QObject *fo = self.focusObject) { QInputMethodQueryEvent queryEvent(Qt::ImEnabled); if (QCoreApplication::sendEvent(fo, &queryEvent)) { if (queryEvent.value(Qt::ImEnabled).toBool()) { @@ -82,6 +82,17 @@ @implementation QNSView (ComplexText) +- (QObject*)focusObject +{ + // The text input system may still hold a reference to our QNSView, + // even after QCocoaWindow has been destructed, delivering text input + // events to us, so we need to guard for this situation explicitly. + if (!m_platformWindow) + return nullptr; + + return m_platformWindow->window()->focusObject(); +} + - (void)insertNewline:(id)sender { Q_UNUSED(sender); @@ -110,7 +121,7 @@ commitString = QString::fromCFString(reinterpret_cast<CFStringRef>(aString)); }; } - if (QObject *fo = m_platformWindow->window()->focusObject()) { + if (QObject *fo = self.focusObject) { QInputMethodQueryEvent queryEvent(Qt::ImEnabled); if (QCoreApplication::sendEvent(fo, &queryEvent)) { if (queryEvent.value(Qt::ImEnabled).toBool()) { @@ -178,7 +189,7 @@ m_composingText = preeditString; - if (QObject *fo = m_platformWindow->window()->focusObject()) { + if (QObject *fo = self.focusObject) { m_composingFocusObject = fo; QInputMethodQueryEvent queryEvent(Qt::ImEnabled); if (QCoreApplication::sendEvent(fo, &queryEvent)) { @@ -200,7 +211,7 @@ - (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)aRange actualRange:(NSRangePointer)actualRange { Q_UNUSED(actualRange) - QObject *fo = m_platformWindow->window()->focusObject(); + QObject *fo = self.focusObject; if (!fo) return nil; QInputMethodQueryEvent queryEvent(Qt::ImEnabled | Qt::ImCurrentSelection); @@ -235,7 +246,7 @@ { NSRange selectedRange = {0, 0}; - QObject *fo = m_platformWindow->window()->focusObject(); + QObject *fo = self.focusObject; if (!fo) return selectedRange; QInputMethodQueryEvent queryEvent(Qt::ImEnabled | Qt::ImCurrentSelection); @@ -258,7 +269,7 @@ Q_UNUSED(aRange) Q_UNUSED(actualRange) - QObject *fo = m_platformWindow->window()->focusObject(); + QObject *fo = self.focusObject; if (!fo) return NSZeroRect; diff --git a/src/plugins/platforms/cocoa/qnsview_dragging.mm b/src/plugins/platforms/cocoa/qnsview_dragging.mm index 978d73f7d9..a4e4ad2f62 100644 --- a/src/plugins/platforms/cocoa/qnsview_dragging.mm +++ b/src/plugins/platforms/cocoa/qnsview_dragging.mm @@ -297,7 +297,9 @@ static QPoint mapWindowCoordinates(QWindow *source, QWindow *target, QPoint poin QCocoaDrag* nativeDrag = QCocoaIntegration::instance()->drag(); Q_ASSERT(nativeDrag); nativeDrag->exitDragLoop(); - nativeDrag->setAcceptedAction(qt_mac_mapNSDragOperation(operation)); + // for internal drag'n'drop, don't override the action the drop event accepted + if (!nativeDrag->currentDrag()) + nativeDrag->setAcceptedAction(qt_mac_mapNSDragOperation(operation)); // Qt starts drag-and-drop on a mouse button press event. Cococa in // this case won't send the matching release event, so we have to diff --git a/src/plugins/platforms/cocoa/qnsview_drawing.mm b/src/plugins/platforms/cocoa/qnsview_drawing.mm index 2fd63fad67..537ea2aef4 100644 --- a/src/plugins/platforms/cocoa/qnsview_drawing.mm +++ b/src/plugins/platforms/cocoa/qnsview_drawing.mm @@ -73,8 +73,15 @@ // by AppKit at a point where we've already set up other parts of the platform plugin // based on the presence of layers or not. Once we've rewritten these parts to support // dynamically picking up layer enablement we can let AppKit do its thing. - return QMacVersion::buildSDK() >= QOperatingSystemVersion::MacOSMojave - && QMacVersion::currentRuntime() >= QOperatingSystemVersion::MacOSMojave; + + if (QMacVersion::currentRuntime() >= QOperatingSystemVersion::MacOSBigSur) + return true; // Big Sur always enables layer-backing, regardless of SDK + + if (QMacVersion::currentRuntime() >= QOperatingSystemVersion::MacOSMojave + && QMacVersion::buildSDK() >= QOperatingSystemVersion::MacOSMojave) + return true; // Mojave and Catalina enable layers based on the app's SDK + + return false; // Prior versions needed explicitly enabled layer backing } - (BOOL)layerExplicitlyRequested diff --git a/src/plugins/platforms/cocoa/qnsview_menus.mm b/src/plugins/platforms/cocoa/qnsview_menus.mm index 7ae274ab04..8cfac5556a 100644 --- a/src/plugins/platforms/cocoa/qnsview_menus.mm +++ b/src/plugins/platforms/cocoa/qnsview_menus.mm @@ -73,19 +73,21 @@ static bool selectorIsCutCopyPaste(SEL selector) if (platformItem->menu()) return YES; - // Check if a modal dialog is active. Validate only menu - // items belonging to this view's window own menu bar. - if (QGuiApplication::modalWindow()) { + // Check if a modal dialog is active. If so, enable only menu + // items explicitly belonging to this window's own menu bar, or to the window. + if (QGuiApplication::modalWindow() && QGuiApplication::modalWindow()->isActive()) { QCocoaMenuBar *menubar = nullptr; + QCocoaWindow *menuWindow = nullptr; QObject *menuParent = platformItem->menuParent(); while (menuParent && !(menubar = qobject_cast<QCocoaMenuBar *>(menuParent))) { + menuWindow = qobject_cast<QCocoaWindow *>(menuParent); auto *menuObject = dynamic_cast<QCocoaMenuObject *>(menuParent); - menuParent = menuObject->menuParent(); + menuParent = menuObject ? menuObject->menuParent() : nullptr; } - // we have no menubar parent for the application menu items, e.g About and Preferences - if (!menubar || menubar->cocoaWindow() != self.platformWindow) + if ((!menuWindow || menuWindow->window() != QGuiApplication::modalWindow()) + && (!menubar || menubar->cocoaWindow() != self.platformWindow)) return NO; } diff --git a/src/plugins/platforms/cocoa/qnsview_mouse.mm b/src/plugins/platforms/cocoa/qnsview_mouse.mm index b42f2d0e7e..81f2e4fd58 100644 --- a/src/plugins/platforms/cocoa/qnsview_mouse.mm +++ b/src/plugins/platforms/cocoa/qnsview_mouse.mm @@ -93,7 +93,7 @@ - (void)resetMouseButtons { - qCDebug(lcQpaMouse) << "Reseting mouse buttons"; + qCDebug(lcQpaMouse) << "Resetting mouse buttons"; m_buttons = Qt::NoButton; m_frameStrutButtons = Qt::NoButton; } @@ -103,21 +103,14 @@ if (!m_platformWindow) return; - // get m_buttons in sync - // Don't send frme strut events if we are in the middle of a mouse drag. - if (m_buttons != Qt::NoButton) - return; - switch (theEvent.type) { case NSEventTypeLeftMouseDown: - case NSEventTypeLeftMouseDragged: m_frameStrutButtons |= Qt::LeftButton; break; case NSEventTypeLeftMouseUp: m_frameStrutButtons &= ~Qt::LeftButton; break; case NSEventTypeRightMouseDown: - case NSEventTypeRightMouseDragged: m_frameStrutButtons |= Qt::RightButton; break; case NSEventTypeRightMouseUp: @@ -132,6 +125,22 @@ break; } + // m_buttons can sometimes get out of sync with the button state in AppKit + // E.g if the QNSView where a drag starts is reparented to another window + // while the drag is ongoing, it will not get the corresponding mouseUp + // call. This will result in m_buttons to be stuck on Qt::LeftButton. + // Since we know which buttons was pressed/released directly on the frame + // strut, we can rectify m_buttons here so that we at least don't return early + // from the drag test underneath because of the faulty m_buttons state. + // FIXME: get m_buttons in sync with AppKit/NSEvent all over in QNSView. + m_buttons &= ~m_frameStrutButtons; + + if (m_buttons != Qt::NoButton) { + // Don't send frame strut events if we are in the middle of + // a mouse drag that didn't start on the frame strut. + return; + } + NSWindow *window = [self window]; NSPoint windowPoint = [theEvent locationInWindow]; @@ -176,6 +185,39 @@ QWindowSystemInterface::handleFrameStrutMouseEvent(m_platformWindow->window(), timestamp, qtWindowPoint, qtScreenPoint, m_frameStrutButtons, button, eventType); } + +- (bool)closePopups:(NSEvent *)theEvent +{ + QList<QCocoaWindow *> *popups = QCocoaIntegration::instance()->popupWindowStack(); + if (!popups->isEmpty()) { + // Check if the click is outside all popups. + bool inside = false; + QPointF qtScreenPoint = QCocoaScreen::mapFromNative([self screenMousePoint:theEvent]); + for (QList<QCocoaWindow *>::const_iterator it = popups->begin(); it != popups->end(); ++it) { + if ((*it)->geometry().contains(qtScreenPoint.toPoint())) { + inside = true; + break; + } + } + // Close the popups if the click was outside. + if (!inside) { + bool selfClosed = false; + Qt::WindowType type = QCocoaIntegration::instance()->activePopupWindow()->window()->type(); + while (QCocoaWindow *popup = QCocoaIntegration::instance()->popPopupWindow()) { + selfClosed = self == popup->view(); + QWindowSystemInterface::handleCloseEvent(popup->window()); + QWindowSystemInterface::flushWindowSystemEvents(); + if (!m_platformWindow) + return true; // Bail out if window was destroyed + } + // Consume the mouse event when closing the popup, except for tool tips + // were it's expected that the event is processed normally. + if (type != Qt::ToolTip || selfClosed) + return true; + } + } + return false; +} @end @implementation QNSView (Mouse) @@ -381,34 +423,8 @@ // that particular poup type (for example context menus). However, Qt expects // that plain popup QWindows will also be closed, so we implement the logic // here as well. - QList<QCocoaWindow *> *popups = QCocoaIntegration::instance()->popupWindowStack(); - if (!popups->isEmpty()) { - // Check if the click is outside all popups. - bool inside = false; - QPointF qtScreenPoint = QCocoaScreen::mapFromNative([self screenMousePoint:theEvent]); - for (QList<QCocoaWindow *>::const_iterator it = popups->begin(); it != popups->end(); ++it) { - if ((*it)->geometry().contains(qtScreenPoint.toPoint())) { - inside = true; - break; - } - } - // Close the popups if the click was outside. - if (!inside) { - bool selfClosed = false; - Qt::WindowType type = QCocoaIntegration::instance()->activePopupWindow()->window()->type(); - while (QCocoaWindow *popup = QCocoaIntegration::instance()->popPopupWindow()) { - selfClosed = self == popup->view(); - QWindowSystemInterface::handleCloseEvent(popup->window()); - QWindowSystemInterface::flushWindowSystemEvents(); - if (!m_platformWindow) - return; // Bail out if window was destroyed - } - // Consume the mouse event when closing the popup, except for tool tips - // were it's expected that the event is processed normally. - if (type != Qt::ToolTip || selfClosed) - return; - } - } + if ([self closePopups:theEvent]) + return; QPointF qtWindowPoint; QPointF qtScreenPoint; @@ -655,14 +671,18 @@ // had time to emit a momentum phase event. if ([NSApp nextEventMatchingMask:NSEventMaskScrollWheel untilDate:[NSDate distantPast] inMode:@"QtMomementumEventSearchMode" dequeue:NO].momentumPhase == NSEventPhaseBegan) { - Q_ASSERT(pixelDelta.isNull() && angleDelta.isNull()); - return; // Ignore this event, as it has a delta of 0,0 + return; // Ignore, even if it has delta + } else { + phase = Qt::ScrollEnd; + m_scrolling = false; } - phase = Qt::ScrollEnd; - m_scrolling = false; } else if (theEvent.momentumPhase == NSEventPhaseBegan) { Q_ASSERT(!pixelDelta.isNull() && !angleDelta.isNull()); - phase = Qt::ScrollUpdate; // Send as update, it has a delta + // If we missed finding a momentum NSEventPhaseBegan when the non-momentum + // phase ended we need to treat this as a scroll begin, to not confuse client + // code. Otherwise we treat it as a continuation of the existing scroll. + phase = m_scrolling ? Qt::ScrollUpdate : Qt::ScrollBegin; + m_scrolling = true; } else if (theEvent.momentumPhase == NSEventPhaseChanged) { phase = Qt::ScrollMomentum; } else if (theEvent.phase == NSEventPhaseCancelled @@ -674,6 +694,16 @@ Q_ASSERT(theEvent.momentumPhase != NSEventPhaseStationary); } + // Sanitize deltas for events that should not result in scrolling. + // On macOS 12.1 this phase has been observed to report deltas. + if (theEvent.phase == NSEventPhaseCancelled) { + if (!pixelDelta.isNull() || !angleDelta.isNull()) { + qCInfo(lcQpaMouse) << "Ignoring unexpected delta for" << theEvent; + pixelDelta = QPoint(); + angleDelta = QPoint(); + } + } + // Prevent keyboard modifier state from changing during scroll event streams. // A two-finger trackpad flick generates a stream of scroll events. We want // the keyboard modifier state to be the state at the beginning of the diff --git a/src/plugins/platforms/cocoa/qnsview_tablet.mm b/src/plugins/platforms/cocoa/qnsview_tablet.mm index ba1fa55892..f365403502 100644 --- a/src/plugins/platforms/cocoa/qnsview_tablet.mm +++ b/src/plugins/platforms/cocoa/qnsview_tablet.mm @@ -148,9 +148,6 @@ static QTabletEvent::TabletDevice wacomTabletDevice(NSEvent *theEvent) device = QTabletEvent::Stylus; } else { switch (bits & 0x0F06) { - case 0x0802: - device = QTabletEvent::Stylus; - break; case 0x0902: device = QTabletEvent::Airbrush; break; @@ -163,8 +160,8 @@ static QTabletEvent::TabletDevice wacomTabletDevice(NSEvent *theEvent) case 0x0804: device = QTabletEvent::RotationStylus; break; - default: - device = QTabletEvent::NoDevice; + default: // usually 0x0802, but 0 on iPad sidecar with Apple Pencil + device = QTabletEvent::Stylus; } } return device; diff --git a/src/plugins/platforms/cocoa/qnswindow.mm b/src/plugins/platforms/cocoa/qnswindow.mm index 1756d429ea..75235a9863 100644 --- a/src/plugins/platforms/cocoa/qnswindow.mm +++ b/src/plugins/platforms/cocoa/qnswindow.mm @@ -104,7 +104,7 @@ static bool isMouseEvent(NSEvent *ev) // Unfortunately there's no NSWindowListOrderedBackToFront, // so we have to manually reverse the order using an array. - NSMutableArray<NSWindow *> *windows = [NSMutableArray<NSWindow *> new]; + NSMutableArray<NSWindow *> *windows = [[NSMutableArray<NSWindow *> new] autorelease]; [application enumerateWindowsWithOptions:NSWindowListOrderedFrontToBack usingBlock:^(NSWindow *window, BOOL *) { // For some reason AppKit will give us nil-windows, skip those @@ -178,6 +178,14 @@ static bool isMouseEvent(NSEvent *ev) if (!NSApp.modalWindow) return NO; + // Special case popup windows (menus, completions, etc), as these usually + // don't have a transient parent set, and we don't want to block them. The + // assumption is that these windows are only opened intermittently, from + // within windows that can already be interacted with in this modal session. + Qt::WindowType type = m_platformWindow->window()->type(); + if (type == Qt::Popup) + return YES; + // If the current modal window (top level modal session) is not a Qt window we // have no way of knowing if this window is transient child of the modal window. if (![NSApp.modalWindow conformsToProtocol:@protocol(QNSWindowProtocol)]) @@ -313,8 +321,18 @@ OSStatus CGSClearWindowTags(const CGSConnectionID, const CGSWindowID, int *, int - (NSColor *)backgroundColor { - return self.styleMask == NSWindowStyleMaskBorderless ? - [NSColor clearColor] : [super backgroundColor]; + // FIXME: Plumb to a WA_NoSystemBackground-like window flag, + // or a QWindow::backgroundColor() property. In the meantime + // we assume that if you have translucent content, without a + // frame then you intend to do all background drawing yourself. + const QWindow *window = m_platformWindow ? m_platformWindow->window() : nullptr; + if (!self.opaque && window && window->flags().testFlag(Qt::FramelessWindowHint)) + return [NSColor clearColor]; + + // This still allows you to have translucent content with a frame, + // where the system background (or color set via NSWindow) will + // shine through. + return [super backgroundColor]; } - (void)sendEvent:(NSEvent*)theEvent @@ -339,18 +357,29 @@ OSStatus CGSClearWindowTags(const CGSConnectionID, const CGSWindowID, int *, int return; } + const bool mouseEventInFrameStrut = [theEvent, self]{ + if (isMouseEvent(theEvent)) { + const NSPoint loc = theEvent.locationInWindow; + const NSRect windowFrame = [self convertRectFromScreen:self.frame]; + const NSRect contentFrame = self.contentView.frame; + if (NSMouseInRect(loc, windowFrame, NO) && !NSMouseInRect(loc, contentFrame, NO)) + return true; + } + return false; + }(); + // Any mouse-press in the frame of the window, including the title bar buttons, should + // close open popups. Presses within the window's content are handled to do that in the + // NSView::mouseDown implementation. + if (theEvent.type == NSEventTypeLeftMouseDown && mouseEventInFrameStrut) + [qnsview_cast(m_platformWindow->view()) closePopups:theEvent]; + [super sendEvent:theEvent]; if (!m_platformWindow) return; // Platform window went away while processing event - if (m_platformWindow->frameStrutEventsEnabled() && isMouseEvent(theEvent)) { - NSPoint loc = [theEvent locationInWindow]; - NSRect windowFrame = [self convertRectFromScreen:self.frame]; - NSRect contentFrame = self.contentView.frame; - if (NSMouseInRect(loc, windowFrame, NO) && !NSMouseInRect(loc, contentFrame, NO)) - [qnsview_cast(m_platformWindow->view()) handleFrameStrutMouseEvent:theEvent]; - } + if (m_platformWindow->frameStrutEventsEnabled() && mouseEventInFrameStrut) + [qnsview_cast(m_platformWindow->view()) handleFrameStrutMouseEvent:theEvent]; } - (void)closeAndRelease |