diff options
Diffstat (limited to 'src/plugins/platforms/cocoa')
47 files changed, 1429 insertions, 453 deletions
diff --git a/src/plugins/platforms/cocoa/CMakeLists.txt b/src/plugins/platforms/cocoa/CMakeLists.txt index af8434daaa..491c61703f 100644 --- a/src/plugins/platforms/cocoa/CMakeLists.txt +++ b/src/plugins/platforms/cocoa/CMakeLists.txt @@ -7,7 +7,7 @@ qt_internal_add_plugin(QCocoaIntegrationPlugin OUTPUT_NAME qcocoa - DEFAULT_IF ${QT_QPA_DEFAULT_PLATFORM} MATCHES cocoa + DEFAULT_IF "cocoa" IN_LIST QT_QPA_PLATFORMS PLUGIN_TYPE platforms SOURCES main.mm @@ -56,6 +56,7 @@ qt_internal_add_plugin(QCocoaIntegrationPlugin ${FWIOSurface} ${FWMetal} ${FWQuartzCore} + ${FWUniformTypeIdentifiers} Qt::Core Qt::CorePrivate Qt::Gui diff --git a/src/plugins/platforms/cocoa/qcocoaaccessibility.mm b/src/plugins/platforms/cocoa/qcocoaaccessibility.mm index c5e40a4087..40c1e90511 100644 --- a/src/plugins/platforms/cocoa/qcocoaaccessibility.mm +++ b/src/plugins/platforms/cocoa/qcocoaaccessibility.mm @@ -36,6 +36,23 @@ void QCocoaAccessibility::notifyAccessibilityUpdate(QAccessibleEvent *event) } switch (event->type()) { + case QAccessible::Announcement: { + auto *announcementEvent = static_cast<QAccessibleAnnouncementEvent *>(event); + auto priorityLevel = (announcementEvent->priority() == QAccessible::AnnouncementPriority::Assertive) + ? NSAccessibilityPriorityHigh + : NSAccessibilityPriorityMedium; + NSDictionary *announcementInfo = @{ + NSAccessibilityPriorityKey: [NSNumber numberWithInt:priorityLevel], + NSAccessibilityAnnouncementKey: announcementEvent->message().toNSString() + }; + // post event for application element, as the comment for + // NSAccessibilityAnnouncementRequestedNotification in the + // NSAccessibilityConstants.h header says + NSAccessibilityPostNotificationWithUserInfo(NSApp, + NSAccessibilityAnnouncementRequestedNotification, + announcementInfo); + break; + } case QAccessible::Focus: { NSAccessibilityPostNotification(element, NSAccessibilityFocusedUIElementChangedNotification); break; diff --git a/src/plugins/platforms/cocoa/qcocoaaccessibilityelement.mm b/src/plugins/platforms/cocoa/qcocoaaccessibilityelement.mm index 27fd32f91f..b319dd072e 100644 --- a/src/plugins/platforms/cocoa/qcocoaaccessibilityelement.mm +++ b/src/plugins/platforms/cocoa/qcocoaaccessibilityelement.mm @@ -9,12 +9,17 @@ #include "qcocoawindow.h" #include "qcocoascreen.h" +#include <QtCore/qlogging.h> #include <QtGui/private/qaccessiblecache_p.h> #include <QtGui/private/qaccessiblebridgeutils_p.h> #include <QtGui/qaccessible.h> QT_USE_NAMESPACE +Q_LOGGING_CATEGORY(lcAccessibilityTable, "qt.accessibility.table") + +using namespace Qt::Literals::StringLiterals; + #if QT_CONFIG(accessibility) /** @@ -130,12 +135,37 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of if (tableInterface) { auto *tableElement = [QMacAccessibilityElement elementWithInterface:table]; Q_ASSERT(tableElement); - Q_ASSERT(tableElement->rows && int(tableElement->rows.count) > m_rowIndex); + if (!tableElement->rows + || int(tableElement->rows.count) <= m_rowIndex + || int(tableElement->rows.count) != tableInterface->rowCount()) { + qCWarning(lcAccessibilityTable) + << "Cell requested for row" << m_rowIndex << "is out of" + << "bounds for table with" << (tableElement->rows ? + tableElement->rows.count : tableInterface->rowCount()) + << "rows! Resizing table model."; + [tableElement updateTableModel]; + } + + Q_ASSERT(tableElement->rows); + Q_ASSERT(int(tableElement->rows.count) > m_rowIndex); + auto *rowElement = tableElement->rows[m_rowIndex]; - if (!rowElement->columns) { + if (!rowElement->columns || int(rowElement->columns.count) != tableInterface->columnCount()) { + if (rowElement->columns) { + qCWarning(lcAccessibilityTable) + << "Table representation column count is out of sync:" + << rowElement->columns.count << "!=" << tableInterface->columnCount(); + } rowElement->columns = [rowElement populateTableRow:rowElement->columns count:tableInterface->columnCount()]; } + + qCDebug(lcAccessibilityTable) << "Creating cell representation for" + << m_rowIndex << m_columnIndex + << "in table with" + << tableElement->rows.count << "rows and" + << rowElement->columns.count << "columns"; + rowElement->columns[m_columnIndex] = self; } } @@ -211,6 +241,10 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of - (NSMutableArray *)populateTableArray:(NSMutableArray *)array role:(NSAccessibilityRole)role count:(int)count { if (QAccessibleInterface *iface = self.qtInterface) { + if (array && int(array.count) != count) { + [array release]; + array = nil; + } if (!array) { array = [NSMutableArray<QMacAccessibilityElement *> arrayWithCapacity:count]; [array retain]; @@ -241,6 +275,11 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of - (NSMutableArray *)populateTableRow:(NSMutableArray *)array count:(int)count { Q_ASSERT(synthesizedRole == NSAccessibilityRowRole); + if (array && int(array.count) != count) { + [array release]; + array = nil; + } + if (!array) { array = [NSMutableArray<QMacAccessibilityElement *> arrayWithCapacity:count]; [array retain]; @@ -272,6 +311,8 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of if (QAccessibleInterface *iface = self.qtInterface) { if (QAccessibleTableInterface *table = iface->tableInterface()) { Q_ASSERT(!self.isManagedByParent); + qCDebug(lcAccessibilityTable) << "Updating table representation with" + << table->rowCount() << table->columnCount(); rows = [self populateTableArray:rows role:NSAccessibilityRowRole count:table->rowCount()]; columns = [self populateTableArray:columns role:NSAccessibilityColumnRole count:table->columnCount()]; } @@ -434,6 +475,30 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of return QCocoaAccessible::unignoredChildren(iface); } +- (NSArray *) accessibilitySelectedChildren { + QAccessibleInterface *iface = QAccessible::accessibleInterface(axid); + if (!iface || !iface->isValid()) + return nil; + + QAccessibleSelectionInterface *selection = iface->selectionInterface(); + if (!selection) + return nil; + + const QList<QAccessibleInterface *> selectedList = selection->selectedItems(); + const qsizetype numSelected = selectedList.size(); + NSMutableArray<QMacAccessibilityElement *> *selectedChildren = + [NSMutableArray<QMacAccessibilityElement *> arrayWithCapacity:numSelected]; + for (QAccessibleInterface *selectedChild : selectedList) { + if (selectedChild && selectedChild->isValid()) { + QAccessible::Id id = QAccessible::uniqueId(selectedChild); + QMacAccessibilityElement *element = [QMacAccessibilityElement elementWithId:id]; + if (element) + [selectedChildren addObject:element]; + } + } + return NSAccessibilityUnignoredChildren(selectedChildren); +} + - (id) accessibilityWindow { // We're in the same window as our parent. return [self.accessibilityParent accessibilityWindow]; @@ -455,6 +520,12 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of return nil; } +- (NSString*) accessibilityIdentifier { + if (QAccessibleInterface *iface = self.qtInterface) + return QAccessibleBridgeUtils::accessibleId(iface).toNSString(); + return nil; +} + - (BOOL) isAccessibilityEnabled { if (QAccessibleInterface *iface = self.qtInterface) return !iface->state().disabled; @@ -466,7 +537,8 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of // a synthetic cell without interface - shortcut to the row QMacAccessibilityElement *tableElement = [QMacAccessibilityElement elementWithId:axid]; - Q_ASSERT(tableElement && tableElement->rows && int(tableElement->rows.count) > m_rowIndex); + Q_ASSERT(tableElement && tableElement->rows); + Q_ASSERT(int(tableElement->rows.count) > m_rowIndex); QMacAccessibilityElement *rowElement = tableElement->rows[m_rowIndex]; return rowElement; } @@ -496,7 +568,9 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of rowIndex = m_rowIndex; else if (QAccessibleTableCellInterface *cell = iface->tableCellInterface()) rowIndex = cell->rowIndex(); - Q_ASSERT(tableElement->rows && int([tableElement->rows count]) > rowIndex); + Q_ASSERT(tableElement->rows); + if (rowIndex > int([tableElement->rows count]) || rowIndex == -1) + return nil; QMacAccessibilityElement *rowElement = tableElement->rows[rowIndex]; return NSAccessibilityUnignoredAncestor(rowElement); } @@ -714,7 +788,7 @@ static void convertLineOffset(QAccessibleTextInterface *text, int *line, int *of QRectF rect; if (range.length > 0) { NSUInteger position = range.location + range.length - 1; - if (position > range.location && iface->textInterface()->text(position, position + 1) == QStringLiteral("\n")) + if (position > range.location && iface->textInterface()->text(position, position + 1) == "\n"_L1) --position; QRect lastRect = iface->textInterface()->characterRect(position); rect = firstRect.united(lastRect); diff --git a/src/plugins/platforms/cocoa/qcocoaapplicationdelegate.mm b/src/plugins/platforms/cocoa/qcocoaapplicationdelegate.mm index bb7b5c3c0c..d642115926 100644 --- a/src/plugins/platforms/cocoa/qcocoaapplicationdelegate.mm +++ b/src/plugins/platforms/cocoa/qcocoaapplicationdelegate.mm @@ -148,7 +148,8 @@ QT_USE_NAMESPACE - (void)applicationWillFinishLaunching:(NSNotification *)notification { - Q_UNUSED(notification); + if ([reflectionDelegate respondsToSelector:_cmd]) + [reflectionDelegate applicationWillFinishLaunching:notification]; /* From the Cocoa documentation: "A good place to install event handlers @@ -185,15 +186,34 @@ QT_USE_NAMESPACE - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { - Q_UNUSED(aNotification); + if ([reflectionDelegate respondsToSelector:_cmd]) + [reflectionDelegate applicationDidFinishLaunching:aNotification]; + inLaunch = false; if (qEnvironmentVariableIsEmpty("QT_MAC_DISABLE_FOREGROUND_APPLICATION_TRANSFORM")) { - // Move the application window to front to avoid launching behind the terminal. - // Ignoring other apps is necessary (we must ignore the terminal), but makes - // Qt apps play slightly less nice with other apps when lanching from Finder - // (See the activateIgnoringOtherApps docs.) - [[NSApplication sharedApplication] activateIgnoringOtherApps:YES]; + auto frontmostApplication = NSWorkspace.sharedWorkspace.frontmostApplication; + auto currentApplication = NSRunningApplication.currentApplication; + if (frontmostApplication != currentApplication) { + // Move the application to front to avoid launching behind the terminal. + // Ignoring other apps is necessary (we must ignore the terminal), but makes + // Qt apps play slightly less nice with other apps when launching from Finder + // (see the activateIgnoringOtherApps docs). FIXME: Try to distinguish between + // being non-active here because another application stole activation in the + // time it took us to launch from Finder, and being non-active because we were + // launched from Terminal or something that doesn't activate us at all. + qCDebug(lcQpaApplication) << "Launched with" << frontmostApplication + << "as frontmost application. Activating" << currentApplication << "instead."; + [NSApplication.sharedApplication activateIgnoringOtherApps:YES]; + } + + // Qt windows are typically shown in main(), at which point the application + // is not active yet. When the application is activated, either externally + // or via the override above, it will only bring the main and key windows + // forward, which differs from the behavior if these windows had been shown + // once the application was already active. To work around this, we explicitly + // activate the current application again, bringing all windows to the front. + [currentApplication activateWithOptions:NSApplicationActivateAllWindows]; } QCocoaMenuBar::insertWindowMenu(); @@ -314,21 +334,72 @@ QT_USE_NAMESPACE [self doesNotRecognizeSelector:invocationSelector]; } +- (BOOL)application:(NSApplication *)application continueUserActivity:(NSUserActivity *)userActivity + restorationHandler:(void(^)(NSArray<id<NSUserActivityRestoring>> *restorableObjects))restorationHandler +{ + // Check if eg. user has installed an app delegate capable of handling this + if ([reflectionDelegate respondsToSelector:_cmd] + && [reflectionDelegate application:application continueUserActivity:userActivity + restorationHandler:restorationHandler] == YES) { + return YES; + } + + if (!QGuiApplication::instance()) + return NO; + + if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) { + QCocoaIntegration *cocoaIntegration = QCocoaIntegration::instance(); + Q_ASSERT(cocoaIntegration); + return cocoaIntegration->services()->handleUrl(QUrl::fromNSURL(userActivity.webpageURL)); + } + + return NO; +} + - (void)getUrl:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent { Q_UNUSED(replyEvent); + NSString *urlString = [[event paramDescriptorForKeyword:keyDirectObject] stringValue]; + const QString qurlString = QString::fromNSString(urlString); + + if (event.eventClass == kInternetEventClass && event.eventID == kAEGetURL) { + // 'GURL' (Get URL) event this application should handle + if (!QGuiApplication::instance()) + return; + QCocoaIntegration *cocoaIntegration = QCocoaIntegration::instance(); + Q_ASSERT(cocoaIntegration); + cocoaIntegration->services()->handleUrl(QUrl(qurlString)); + return; + } + // The string we get from the requesting application might not necessarily meet // QUrl's requirement for a IDN-compliant host. So if we can't parse into a QUrl, // then we pass the string on to the application as the name of a file (and // QFileOpenEvent::file is not guaranteed to be the path to a local, open'able // file anyway). - const QString qurlString = QString::fromNSString(urlString); if (const QUrl url(qurlString); url.isValid()) QWindowSystemInterface::handleFileOpenEvent(url); else QWindowSystemInterface::handleFileOpenEvent(qurlString); } + +- (BOOL)applicationSupportsSecureRestorableState:(NSApplication *)application +{ + if (@available(macOS 12, *)) { + if ([reflectionDelegate respondsToSelector:_cmd]) + return [reflectionDelegate applicationSupportsSecureRestorableState:application]; + } + + // We don't support or implement state restorations via the AppKit + // state restoration APIs, but if we did, we would/should support + // secure state restoration. This is the default for apps linked + // against the macOS 14 SDK, but as we target versions below that + // as well we need to return YES here explicitly to silence a runtime + // warning. + return YES; +} + @end @implementation QCocoaApplicationDelegate (Menus) @@ -371,7 +442,6 @@ QT_USE_NAMESPACE if (!platformItem || platformItem->menu()) return; - QScopedScopeLevelCounter scopeLevelCounter(QGuiApplicationPrivate::instance()->threadData.loadRelaxed()); QGuiApplicationPrivate::modifier_buttons = QAppleKeyMapper::fromCocoaModifiers([NSEvent modifierFlags]); static QMetaMethod activatedSignal = QMetaMethod::fromSignal(&QCocoaMenuItem::activated); diff --git a/src/plugins/platforms/cocoa/qcocoabackingstore.h b/src/plugins/platforms/cocoa/qcocoabackingstore.h index 6db88f923c..71b6015a54 100644 --- a/src/plugins/platforms/cocoa/qcocoabackingstore.h +++ b/src/plugins/platforms/cocoa/qcocoabackingstore.h @@ -54,6 +54,7 @@ private: bool eventFilter(QObject *watched, QEvent *event) override; QSize m_requestedSize; + QRegion m_staticContents; class GraphicsBuffer : public QIOSurfaceGraphicsBuffer { @@ -78,7 +79,8 @@ private: bool recreateBackBufferIfNeeded(); void finalizeBackBuffer(); - void preserveFromFrontBuffer(const QRegion ®ion, const QPoint &offset = QPoint()); + void blitBuffer(GraphicsBuffer *sourceBuffer, const QRegion &sourceRegion, + GraphicsBuffer *destinationBuffer, const QPoint &destinationOffset = QPoint()); void backingPropertiesChanged(); QMacNotificationObserver m_backingPropertiesObserver; diff --git a/src/plugins/platforms/cocoa/qcocoabackingstore.mm b/src/plugins/platforms/cocoa/qcocoabackingstore.mm index 8f50bc5834..b211b5d02d 100644 --- a/src/plugins/platforms/cocoa/qcocoabackingstore.mm +++ b/src/plugins/platforms/cocoa/qcocoabackingstore.mm @@ -23,8 +23,9 @@ QCocoaBackingStore::QCocoaBackingStore(QWindow *window) QCFType<CGColorSpaceRef> QCocoaBackingStore::colorSpace() const { - NSView *view = static_cast<QCocoaWindow *>(window()->handle())->view(); - return QCFType<CGColorSpaceRef>::constructFromGet(view.window.colorSpace.CGColorSpace); + const auto *platformWindow = static_cast<QCocoaWindow *>(window()->handle()); + const QNSView *view = qnsview_cast(platformWindow->view()); + return QCFType<CGColorSpaceRef>::constructFromGet(view.colorSpace.CGColorSpace); } // ---------------------------------------------------------------------------- @@ -72,12 +73,11 @@ bool QCALayerBackingStore::eventFilter(QObject *watched, QEvent *event) void QCALayerBackingStore::resize(const QSize &size, const QRegion &staticContents) { - qCDebug(lcQpaBackingStore) << "Resize requested to" << size; - - if (!staticContents.isNull()) - qCWarning(lcQpaBackingStore) << "QCALayerBackingStore does not support static contents"; + qCDebug(lcQpaBackingStore) << "Resize requested to" << size + << "with static contents" << staticContents; m_requestedSize = size; + m_staticContents = staticContents; } void QCALayerBackingStore::beginPaint(const QRegion ®ion) @@ -189,11 +189,50 @@ bool QCALayerBackingStore::recreateBackBufferIfNeeded() } #endif - qCInfo(lcQpaBackingStore) << "Creating surface of" << requestedBufferSize - << "based on requested" << m_requestedSize << "and dpr =" << devicePixelRatio; + qCInfo(lcQpaBackingStore)<< "Creating surface of" << requestedBufferSize + << "for" << window() << "based on requested" << m_requestedSize + << "dpr =" << devicePixelRatio << "and color space" << colorSpace(); static auto pixelFormat = QImage::toPixelFormat(QImage::Format_ARGB32_Premultiplied); - m_buffers.back().reset(new GraphicsBuffer(requestedBufferSize, devicePixelRatio, pixelFormat, colorSpace())); + auto *newBackBuffer = new GraphicsBuffer(requestedBufferSize, devicePixelRatio, pixelFormat, colorSpace()); + + if (!m_staticContents.isEmpty() && m_buffers.back()) { + // We implicitly support static backingstore content as a result of + // finalizing the back buffer on flush, where we copy any non-painted + // areas from the front buffer. But there is no guarantee that a resize + // will always come after a flush, where we have a pristine front buffer + // to copy from. It may come after a few begin/endPaints, where the back + // buffer then contains (part of) the latest state. We also have the case + // of single-buffered backingstore, where the front and back buffer is + // the same, which means we must do the copy from the old back buffer + // to the newly resized buffer now, before we replace it below. + + // If the back buffer has been partially filled already, we need to + // copy parts of the static content from that. The rest we copy from + // the front buffer. + const QRegion backBufferRegion = m_staticContents - m_buffers.back()->dirtyRegion; + const QRegion frontBufferRegion = m_staticContents - backBufferRegion; + + qCInfo(lcQpaBackingStore) << "Preserving static content" << backBufferRegion + << "from back buffer, and" << frontBufferRegion << "from front buffer"; + + newBackBuffer->lock(QPlatformGraphicsBuffer::SWWriteAccess); + blitBuffer(m_buffers.back().get(), backBufferRegion, newBackBuffer); + Q_ASSERT(frontBufferRegion.isEmpty() || m_buffers.front()); + blitBuffer(m_buffers.front().get(), frontBufferRegion, newBackBuffer); + newBackBuffer->unlock(); + + // The new back buffer now is valid for the static contents region. + // We don't need to maintain the static contents region for resizes + // of any other buffers in the swap chain, as these will finalize + // their content on flush from the buffer we just filled, and we + // don't need to mark them dirty for the area we just filled, as + // new buffers are fully dirty when created. + newBackBuffer->dirtyRegion -= m_staticContents; + m_staticContents = {}; + } + + m_buffers.back().reset(newBackBuffer); return true; } @@ -256,7 +295,7 @@ bool QCALayerBackingStore::scroll(const QRegion ®ion, int dx, int dy) if (!frontBufferRegion.isEmpty()) { qCDebug(lcQpaBackingStore) << "Scrolling" << frontBufferRegion << "by copying from front buffer"; - preserveFromFrontBuffer(frontBufferRegion, scrollDelta); + blitBuffer(m_buffers.front().get(), frontBufferRegion, m_buffers.back().get(), scrollDelta); } m_buffers.back()->unlock(); @@ -440,10 +479,11 @@ void QCALayerBackingStore::backingPropertiesChanged() qCDebug(lcQpaBackingStore) << "Backing properties for" << window() << "did change"; - qCDebug(lcQpaBackingStore) << "Updating color space of existing buffers"; + const auto newColorSpace = colorSpace(); + qCDebug(lcQpaBackingStore) << "Updating color space of existing buffers to" << newColorSpace; for (auto &buffer : m_buffers) { if (buffer) - buffer->setColorSpace(colorSpace()); + buffer->setColorSpace(newColorSpace); } } @@ -475,54 +515,76 @@ void QCALayerBackingStore::finalizeBackBuffer() if (!m_buffers.back()->isDirty()) return; - m_buffers.back()->lock(QPlatformGraphicsBuffer::SWWriteAccess); - preserveFromFrontBuffer(m_buffers.back()->dirtyRegion); - m_buffers.back()->unlock(); + qCDebug(lcQpaBackingStore) << "Finalizing back buffer with dirty region" << m_buffers.back()->dirtyRegion; + + if (m_buffers.back() != m_buffers.front()) { + m_buffers.back()->lock(QPlatformGraphicsBuffer::SWWriteAccess); + blitBuffer(m_buffers.front().get(), m_buffers.back()->dirtyRegion, m_buffers.back().get()); + m_buffers.back()->unlock(); + } else { + qCDebug(lcQpaBackingStore) << "Front and back buffer is the same. Can not finalize back buffer."; + } // The back buffer is now completely in sync, ready to be presented m_buffers.back()->dirtyRegion = QRegion(); } -void QCALayerBackingStore::preserveFromFrontBuffer(const QRegion ®ion, const QPoint &offset) +/* + \internal + + Blits \a sourceRegion from \a sourceBuffer to \a destinationBuffer, + at offset \a destinationOffset. + + The source buffer is automatically locked for read only access + during the blit. + + The destination buffer has to be locked for write access by the + caller. +*/ + +void QCALayerBackingStore::blitBuffer(GraphicsBuffer *sourceBuffer, const QRegion &sourceRegion, + GraphicsBuffer *destinationBuffer, const QPoint &destinationOffset) { + Q_ASSERT(sourceBuffer && destinationBuffer); + Q_ASSERT(sourceBuffer != destinationBuffer); - if (m_buffers.front() == m_buffers.back()) - return; // Nothing to preserve from + if (sourceRegion.isEmpty()) + return; - qCDebug(lcQpaBackingStore) << "Preserving" << region << "of front buffer to" - << region.translated(offset) << "of back buffer"; + qCDebug(lcQpaBackingStore) << "Blitting" << sourceRegion << "of" << sourceBuffer + << "to" << sourceRegion.translated(destinationOffset) << "of" << destinationBuffer; - Q_ASSERT(m_buffers.back()->isLocked() == QPlatformGraphicsBuffer::SWWriteAccess); + Q_ASSERT(destinationBuffer->isLocked() == QPlatformGraphicsBuffer::SWWriteAccess); - m_buffers.front()->lock(QPlatformGraphicsBuffer::SWReadAccess); - const QImage *frontBuffer = m_buffers.front()->asImage(); + sourceBuffer->lock(QPlatformGraphicsBuffer::SWReadAccess); + const QImage *sourceImage = sourceBuffer->asImage(); - const QRect frontSurfaceBounds(QPoint(0, 0), m_buffers.front()->size()); - const qreal sourceDevicePixelRatio = frontBuffer->devicePixelRatio(); + const QRect sourceBufferBounds(QPoint(0, 0), sourceBuffer->size()); + const qreal sourceDevicePixelRatio = sourceImage->devicePixelRatio(); - QPainter painter(m_buffers.back()->asImage()); + QPainter painter(destinationBuffer->asImage()); painter.setCompositionMode(QPainter::CompositionMode_Source); // Let painter operate in device pixels, to make it easier to compare coordinates - const qreal targetDevicePixelRatio = painter.device()->devicePixelRatio(); - painter.scale(1.0 / targetDevicePixelRatio, 1.0 / targetDevicePixelRatio); + const qreal destinationDevicePixelRatio = painter.device()->devicePixelRatio(); + painter.scale(1.0 / destinationDevicePixelRatio, 1.0 / destinationDevicePixelRatio); - for (const QRect &rect : region) { + for (const QRect &rect : sourceRegion) { QRect sourceRect(rect.topLeft() * sourceDevicePixelRatio, rect.size() * sourceDevicePixelRatio); - QRect targetRect((rect.topLeft() + offset) * targetDevicePixelRatio, - rect.size() * targetDevicePixelRatio); + QRect destinationRect((rect.topLeft() + destinationOffset) * destinationDevicePixelRatio, + rect.size() * destinationDevicePixelRatio); #ifdef QT_DEBUG - if (Q_UNLIKELY(!frontSurfaceBounds.contains(sourceRect.bottomRight()))) { - qCWarning(lcQpaBackingStore) << "Front buffer too small to preserve" - << QRegion(sourceRect).subtracted(frontSurfaceBounds); + if (Q_UNLIKELY(!sourceBufferBounds.contains(sourceRect.bottomRight()))) { + qCWarning(lcQpaBackingStore) << "Source buffer of size" << sourceBuffer->size() + << "is too small to blit" << sourceRect; } #endif - painter.drawImage(targetRect, *frontBuffer, sourceRect); + painter.drawImage(destinationRect, *sourceImage, sourceRect); } - m_buffers.front()->unlock(); + sourceBuffer->unlock(); } // ---------------------------------------------------------------------------- diff --git a/src/plugins/platforms/cocoa/qcocoadrag.mm b/src/plugins/platforms/cocoa/qcocoadrag.mm index de45e8a979..3a9f5a8794 100644 --- a/src/plugins/platforms/cocoa/qcocoadrag.mm +++ b/src/plugins/platforms/cocoa/qcocoadrag.mm @@ -2,13 +2,13 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include <AppKit/AppKit.h> +#include <UniformTypeIdentifiers/UTCoreTypes.h> #include "qcocoadrag.h" #include "qmacclipboard.h" #include "qcocoahelpers.h" #include <QtGui/private/qcoregraphics_p.h> #include <QtGui/qutimimeconverter.h> -#include <QtCore/qsysinfo.h> #include <QtCore/private/qcore_mac_p.h> #include <vector> @@ -136,11 +136,6 @@ bool QCocoaDrag::maybeDragMultipleItems() Q_ASSERT(m_drag && m_drag->mimeData()); Q_ASSERT(m_executed_drop_action == Qt::IgnoreAction); - if (QOperatingSystemVersion::current() < QOperatingSystemVersion::MacOSMojave) { - // -dragImage: stopped working in 10.14 first. - return false; - } - const QMacAutoReleasePool pool; NSView *view = m_lastView ? m_lastView : m_lastEvent.window.contentView; @@ -161,8 +156,7 @@ bool QCocoaDrag::maybeDragMultipleItems() for (NSPasteboardItem *item in dragBoard.pasteboardItems) { bool isUrl = false; for (NSPasteboardType type in item.types) { - using NSStringRef = NSString *; - if ([type isEqualToString:NSStringRef(kUTTypeFileURL)]) { + if ([type isEqualToString:UTTypeFileURL.identifier]) { isUrl = true; break; } @@ -226,9 +220,10 @@ void QCocoaDrag::setAcceptedAction(Qt::DropAction act) void QCocoaDrag::exitDragLoop() { - Q_ASSERT(m_internalDragLoop); - if (m_internalDragLoop->isRunning()) + if (m_internalDragLoop) { + Q_ASSERT(m_internalDragLoop->isRunning()); m_internalDragLoop->exit(); + } } @@ -243,14 +238,14 @@ QPixmap QCocoaDrag::dragPixmap(QDrag *drag, QPoint &hotSpot) const QFontMetrics fm(f); if (data->hasImage()) { - const QImage img = data->imageData().value<QImage>(); + QImage img = data->imageData().value<QImage>(); if (!img.isNull()) { - pm = QPixmap::fromImage(img).scaledToWidth(dragImageMaxChars *fm.averageCharWidth()); + pm = QPixmap::fromImage(std::move(img)).scaledToWidth(dragImageMaxChars *fm.averageCharWidth()); } } if (pm.isNull() && (data->hasText() || data->hasUrls()) ) { - QString s = data->hasText() ? data->text() : data->urls().first().toString(); + QString s = data->hasText() ? data->text() : data->urls().constFirst().toString(); if (s.length() > dragImageMaxChars) s = s.left(dragImageMaxChars -3) + QChar(0x2026); if (!s.isEmpty()) { diff --git a/src/plugins/platforms/cocoa/qcocoaeventdispatcher.h b/src/plugins/platforms/cocoa/qcocoaeventdispatcher.h index 787b23ec4d..96eb70dabc 100644 --- a/src/plugins/platforms/cocoa/qcocoaeventdispatcher.h +++ b/src/plugins/platforms/cocoa/qcocoaeventdispatcher.h @@ -56,9 +56,12 @@ #include <QtCore/private/qcfsocketnotifier_p.h> #include <QtCore/private/qtimerinfo_unix_p.h> #include <QtCore/qloggingcategory.h> +#include <QtCore/qpointer.h> #include <CoreFoundation/CoreFoundation.h> +Q_FORWARD_DECLARE_OBJC_CLASS(NSWindow); + QT_BEGIN_NAMESPACE Q_DECLARE_LOGGING_CATEGORY(lcEventDispatcher); @@ -67,11 +70,11 @@ typedef struct _NSModalSession *NSModalSession; typedef struct _QCocoaModalSessionInfo { QPointer<QWindow> window; NSModalSession session; - void *nswindow; + NSWindow *nswindow; } QCocoaModalSessionInfo; class QCocoaEventDispatcherPrivate; -class QCocoaEventDispatcher : public QAbstractEventDispatcher +class QCocoaEventDispatcher : public QAbstractEventDispatcherV2 { Q_OBJECT Q_DECLARE_PRIVATE(QCocoaEventDispatcher) @@ -86,12 +89,12 @@ public: void registerSocketNotifier(QSocketNotifier *notifier); void unregisterSocketNotifier(QSocketNotifier *notifier); - void registerTimer(int timerId, qint64 interval, Qt::TimerType timerType, QObject *object); - bool unregisterTimer(int timerId); - bool unregisterTimers(QObject *object); - QList<TimerInfo> registeredTimers(QObject *object) const; - - int remainingTime(int timerId); + void registerTimer(Qt::TimerId timerId, Duration interval, Qt::TimerType timerType, + QObject *object) final; + bool unregisterTimer(Qt::TimerId timerId) final; + bool unregisterTimers(QObject *object) final; + QList<TimerInfoV2> timersForObject(QObject *object) const final; + Duration remainingTime(Qt::TimerId timerId) const final; void wakeUp(); void interrupt(); diff --git a/src/plugins/platforms/cocoa/qcocoaeventdispatcher.mm b/src/plugins/platforms/cocoa/qcocoaeventdispatcher.mm index fd0a4b4717..739fbda4f5 100644 --- a/src/plugins/platforms/cocoa/qcocoaeventdispatcher.mm +++ b/src/plugins/platforms/cocoa/qcocoaeventdispatcher.mm @@ -115,6 +115,7 @@ void QCocoaEventDispatcherPrivate::maybeStartCFRunLoopTimer() return; } + using DoubleSeconds = std::chrono::duration<double, std::ratio<1>>; if (!runLoopTimerRef) { // start the CFRunLoopTimer CFAbsoluteTime ttf = CFAbsoluteTimeGetCurrent(); @@ -122,10 +123,10 @@ void QCocoaEventDispatcherPrivate::maybeStartCFRunLoopTimer() CFTimeInterval oneyear = CFTimeInterval(3600. * 24. * 365.); // Q: when should the CFRunLoopTimer fire for the first time? - struct timespec tv; - if (timerInfoList.timerWait(tv)) { + if (auto opt = timerInfoList.timerWait()) { // A: when we have timers to fire, of course - interval = qMax(tv.tv_sec + tv.tv_nsec / 1000000000., 0.0000001); + 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; @@ -145,10 +146,10 @@ void QCocoaEventDispatcherPrivate::maybeStartCFRunLoopTimer() CFTimeInterval interval; // Q: when should the timer first next? - struct timespec tv; - if (timerInfoList.timerWait(tv)) { + if (auto opt = timerInfoList.timerWait()) { // A: when we have timers to fire, of course - interval = qMax(tv.tv_sec + tv.tv_nsec / 1000000000., 0.0000001); + 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) @@ -170,10 +171,11 @@ void QCocoaEventDispatcherPrivate::maybeStopCFRunLoopTimer() runLoopTimerRef = nullptr; } -void QCocoaEventDispatcher::registerTimer(int timerId, qint64 interval, Qt::TimerType timerType, QObject *obj) +void QCocoaEventDispatcher::registerTimer(Qt::TimerId timerId, Duration interval, + Qt::TimerType timerType, QObject *obj) { #ifndef QT_NO_DEBUG - if (timerId < 1 || interval < 0 || !obj) { + if (qToUnderlying(timerId) < 1 || interval.count() < 0 || !obj) { qWarning("QCocoaEventDispatcher::registerTimer: invalid arguments"); return; } else if (obj->thread() != thread() || thread() != QThread::currentThread()) { @@ -187,10 +189,10 @@ void QCocoaEventDispatcher::registerTimer(int timerId, qint64 interval, Qt::Time d->maybeStartCFRunLoopTimer(); } -bool QCocoaEventDispatcher::unregisterTimer(int timerId) +bool QCocoaEventDispatcher::unregisterTimer(Qt::TimerId timerId) { #ifndef QT_NO_DEBUG - if (timerId < 1) { + if (qToUnderlying(timerId) < 1) { qWarning("QCocoaEventDispatcher::unregisterTimer: invalid argument"); return false; } else if (thread() != QThread::currentThread()) { @@ -229,13 +231,13 @@ bool QCocoaEventDispatcher::unregisterTimers(QObject *obj) return returnValue; } -QList<QCocoaEventDispatcher::TimerInfo> -QCocoaEventDispatcher::registeredTimers(QObject *object) const +QList<QCocoaEventDispatcher::TimerInfoV2> +QCocoaEventDispatcher::timersForObject(QObject *object) const { #ifndef QT_NO_DEBUG if (!object) { qWarning("QCocoaEventDispatcher:registeredTimers: invalid argument"); - return QList<TimerInfo>(); + return {}; } #endif @@ -374,6 +376,7 @@ bool QCocoaEventDispatcher::processEvents(QEventLoop::ProcessEventsFlags flags) // [NSApp run], which is the normal code path for cocoa applications. if (NSModalSession session = d->currentModalSession()) { QBoolBlocker 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) { @@ -417,6 +420,7 @@ bool QCocoaEventDispatcher::processEvents(QEventLoop::ProcessEventsFlags flags) // 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 @@ -537,17 +541,17 @@ bool QCocoaEventDispatcher::processEvents(QEventLoop::ProcessEventsFlags flags) return retVal; } -int QCocoaEventDispatcher::remainingTime(int timerId) +auto QCocoaEventDispatcher::remainingTime(Qt::TimerId timerId) const -> Duration { #ifndef QT_NO_DEBUG - if (timerId < 1) { + if (qToUnderlying(timerId) < 1) { qWarning("QCocoaEventDispatcher::remainingTime: invalid argument"); - return -1; + return Duration::min(); } #endif - Q_D(QCocoaEventDispatcher); - return d->timerInfoList.timerRemainingTime(timerId); + Q_D(const QCocoaEventDispatcher); + return d->timerInfoList.remainingDuration(timerId); } void QCocoaEventDispatcher::wakeUp() @@ -617,6 +621,8 @@ void QCocoaEventDispatcherPrivate::temporarilyStopAllModalSessions() 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]; @@ -656,6 +662,8 @@ NSModalSession QCocoaEventDispatcherPrivate::currentModalSession() [(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. @@ -704,6 +712,8 @@ void QCocoaEventDispatcherPrivate::cleanupModalSessions() 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]; } @@ -716,6 +726,14 @@ void QCocoaEventDispatcherPrivate::cleanupModalSessions() 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(). @@ -736,6 +754,8 @@ void QCocoaEventDispatcherPrivate::beginModalSession(QWindow *window) 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 @@ -782,7 +802,7 @@ void qt_mac_maybeCancelWaitForMoreEventsForwarder(QAbstractEventDispatcher *even } QCocoaEventDispatcher::QCocoaEventDispatcher(QObject *parent) - : QAbstractEventDispatcher(*new QCocoaEventDispatcherPrivate, parent) + : QAbstractEventDispatcherV2(*new QCocoaEventDispatcherPrivate, parent) { Q_D(QCocoaEventDispatcher); @@ -957,7 +977,7 @@ QCocoaEventDispatcher::~QCocoaEventDispatcher() { Q_D(QCocoaEventDispatcher); - qDeleteAll(d->timerInfoList); + d->timerInfoList.clearTimers(); d->maybeStopCFRunLoopTimer(); CFRunLoopRemoveSource(mainRunLoop(), d->activateTimersSourceRef, kCFRunLoopCommonModes); CFRelease(d->activateTimersSourceRef); @@ -966,6 +986,8 @@ QCocoaEventDispatcher::~QCocoaEventDispatcher() 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]; } diff --git a/src/plugins/platforms/cocoa/qcocoafiledialoghelper.h b/src/plugins/platforms/cocoa/qcocoafiledialoghelper.h index 85e317b8ef..3ffccb10fd 100644 --- a/src/plugins/platforms/cocoa/qcocoafiledialoghelper.h +++ b/src/plugins/platforms/cocoa/qcocoafiledialoghelper.h @@ -38,6 +38,7 @@ public: public: // for QNSOpenSavePanelDelegate void panelClosed(NSInteger result); + void panelDirectoryDidChange(NSString *path); private: void createNSOpenSavePanelDelegate(); diff --git a/src/plugins/platforms/cocoa/qcocoafiledialoghelper.mm b/src/plugins/platforms/cocoa/qcocoafiledialoghelper.mm index 91d76fa254..41170b74ea 100644 --- a/src/plugins/platforms/cocoa/qcocoafiledialoghelper.mm +++ b/src/plugins/platforms/cocoa/qcocoafiledialoghelper.mm @@ -14,8 +14,6 @@ #include <QtCore/qstringlist.h> #include <QtCore/qvarlengtharray.h> #include <QtCore/qabstracteventdispatcher.h> -#include <QtCore/qsysinfo.h> -#include <QtCore/qoperatingsystemversion.h> #include <QtCore/qdir.h> #include <QtCore/qregularexpression.h> #include <QtCore/qpointer.h> @@ -27,6 +25,8 @@ #include <qpa/qplatformtheme.h> #include <qpa/qplatformnativeinterface.h> +#include <UniformTypeIdentifiers/UniformTypeIdentifiers.h> + QT_USE_NAMESPACE using namespace Qt::StringLiterals; @@ -55,12 +55,11 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions; NSPopUpButton *m_popupButton; NSTextField *m_textField; QPointer<QCocoaFileDialogHelper> m_helper; - NSString *m_currentDirectory; SharedPointerFileDialogOptions m_options; - QString *m_currentSelection; - QStringList *m_nameFilterDropDownList; - QStringList *m_selectedNameFilter; + QString m_currentSelection; + QStringList m_nameFilterDropDownList; + QStringList m_selectedNameFilter; } - (instancetype)initWithAcceptMode:(const QString &)selectFile @@ -80,26 +79,56 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions; m_helper = helper; - m_nameFilterDropDownList = new QStringList(m_options->nameFilters()); + m_nameFilterDropDownList = m_options->nameFilters(); QString selectedVisualNameFilter = m_options->initiallySelectedNameFilter(); - m_selectedNameFilter = new QStringList([self findStrippedFilterWithVisualFilterName:selectedVisualNameFilter]); - - QFileInfo sel(selectFile); + m_selectedNameFilter = [self findStrippedFilterWithVisualFilterName:selectedVisualNameFilter]; + + m_panel.extensionHidden = [&]{ + for (const auto &nameFilter : m_nameFilterDropDownList) { + const auto extensions = QPlatformFileDialogHelper::cleanFilterList(nameFilter); + for (const auto &extension : extensions) { + // Explicitly show extensions if we detect a filter + // of "all files", as clicking a single file with + // extensions hidden will then populate the name + // field with only the file name, without any + // extension. + if (extension == "*"_L1 || extension == "*.*"_L1) + return false; + + // Explicitly show extensions if we detect a filter + // that has a multi-part extension. This prevents + // confusing situations where the user clicks e.g. + // 'foo.tar.gz' and 'foo.tar' is populated in the + // file name box, but when then clicking save macOS + // will warn that the file needs to end in .gz, + // due to thinking the user tried to save the file + // as a 'tar' file instead. Unfortunately this + // property can only be set before the panel is + // shown, so we can't toggle it on and off based + // on the active filter. + if (extension.count('.') > 1) + return false; + } + } + return true; + }(); + + const QFileInfo sel(selectFile); if (sel.isDir() && !sel.isBundle()){ - m_currentDirectory = [sel.absoluteFilePath().toNSString() retain]; - m_currentSelection = new QString; + m_panel.directoryURL = [NSURL fileURLWithPath:sel.absoluteFilePath().toNSString()]; + m_currentSelection.clear(); } else { - m_currentDirectory = [sel.absolutePath().toNSString() retain]; - m_currentSelection = new QString(sel.absoluteFilePath()); + m_panel.directoryURL = [NSURL fileURLWithPath:sel.absolutePath().toNSString()]; + m_currentSelection = sel.absoluteFilePath(); } [self createPopUpButton:selectedVisualNameFilter hideDetails:options->testOption(QFileDialogOptions::HideNameFilterDetails)]; [self createTextField]; [self createAccessory]; - m_panel.accessoryView = m_nameFilterDropDownList->size() > 1 ? m_accessoryView : nil; + m_panel.accessoryView = m_nameFilterDropDownList.size() > 1 ? m_accessoryView : nil; // -setAccessoryView: can result in -panel:directoryDidChange: - // resetting our m_currentDirectory, set the delegate + // resetting our current directory. Set the delegate // here to make sure it gets the correct value. m_panel.delegate = self; @@ -113,10 +142,6 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions; - (void)dealloc { - delete m_nameFilterDropDownList; - delete m_selectedNameFilter; - delete m_currentSelection; - [m_panel orderOut:m_panel]; m_panel.accessoryView = nil; [m_popupButton release]; @@ -124,19 +149,17 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions; [m_accessoryView release]; m_panel.delegate = nil; [m_panel release]; - [m_currentDirectory release]; [super dealloc]; } - (bool)showPanel:(Qt::WindowModality) windowModality withParent:(QWindow *)parent { - QFileInfo info(*m_currentSelection); + const QFileInfo info(m_currentSelection); NSString *filepath = info.filePath().toNSString(); NSURL *url = [NSURL fileURLWithPath:filepath isDirectory:info.isDir()]; bool selectable = (m_options->acceptMode() == QFileDialogOptions::AcceptSave) || [self panel:m_panel shouldEnableURL:url]; - m_panel.directoryURL = [NSURL fileURLWithPath:m_currentDirectory]; m_panel.nameFieldStringValue = selectable ? info.fileName().toNSString() : @""; [self updateProperties]; @@ -184,7 +207,7 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions; - (void)closePanel { - *m_currentSelection = QString::fromNSString(m_panel.URL.path).normalized(QString::NormalizationForm_C); + m_currentSelection = QString::fromNSString(m_panel.URL.path).normalized(QString::NormalizationForm_C); if (m_panel.sheet) [NSApp endSheet:m_panel]; @@ -194,19 +217,6 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions; [m_panel close]; } -- (BOOL)isHiddenFileAtURL:(NSURL *)url -{ - BOOL hidden = NO; - if (url) { - CFBooleanRef isHiddenProperty; - if (CFURLCopyResourcePropertyForKey((__bridge CFURLRef)url, kCFURLIsHiddenKey, &isHiddenProperty, nullptr)) { - hidden = CFBooleanGetValue(isHiddenProperty); - CFRelease(isHiddenProperty); - } - } - return hidden; -} - - (BOOL)panel:(id)sender shouldEnableURL:(NSURL *)url { Q_UNUSED(sender); @@ -215,64 +225,140 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions; if (!filename.length) return NO; - // Always accept directories regardless of their names (unless it is a bundle): - NSFileManager *fm = NSFileManager.defaultManager; - NSDictionary *fileAttrs = [fm attributesOfItemAtPath:filename error:nil]; - if (!fileAttrs) - return NO; // Error accessing the file means 'no'. - NSString *fileType = fileAttrs.fileType; - bool isDir = [fileType isEqualToString:NSFileTypeDirectory]; - if (isDir) { - if (!m_panel.treatsFilePackagesAsDirectories) { - if ([NSWorkspace.sharedWorkspace isFilePackageAtPath:filename] == NO) - return YES; - } + const QFileInfo fileInfo(QString::fromNSString(filename)); + + // Always accept directories regardless of their names. + // This also includes symlinks and aliases to directories. + if (fileInfo.isDir()) { + // Unless it's a bundle, and we should treat bundles as files. + // FIXME: We'd like to use QFileInfo::isBundle() here, but the + // detection in QFileInfo goes deeper than NSWorkspace does + // (likely a bug), and as a result causes TCC permission + // dialogs to pop up when used. + bool treatBundlesAsFiles = !m_panel.treatsFilePackagesAsDirectories; + if (!(treatBundlesAsFiles && [NSWorkspace.sharedWorkspace isFilePackageAtPath:filename])) + return YES; } - // 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 = m_selectedNameFilter->isEmpty(); - // Check if the current file name filter accepts the file: - for (int i = 0; !nameMatches && i < m_selectedNameFilter->size(); ++i) { - if (QDir::match(m_selectedNameFilter->at(i), qtFileName)) - nameMatches = true; - } - if (!nameMatches) + if (![self fileInfoMatchesCurrentNameFilter:fileInfo]) return NO; QDir::Filters filter = m_options->filter(); - if ((!(filter & (QDir::Dirs | QDir::AllDirs)) && isDir) - || (!(filter & QDir::Files) && [fileType isEqualToString:NSFileTypeRegular]) - || ((filter & QDir::NoSymLinks) && [fileType isEqualToString:NSFileTypeSymbolicLink])) + if ((!(filter & (QDir::Dirs | QDir::AllDirs)) && fileInfo.isDir()) + || (!(filter & QDir::Files) && (fileInfo.isFile() && !fileInfo.isSymLink())) + || ((filter & QDir::NoSymLinks) && fileInfo.isSymLink())) return NO; bool filterPermissions = ((filter & QDir::PermissionMask) && (filter & QDir::PermissionMask) != QDir::PermissionMask); if (filterPermissions) { - if ((!(filter & QDir::Readable) && [fm isReadableFileAtPath:filename]) - || (!(filter & QDir::Writable) && [fm isWritableFileAtPath:filename]) - || (!(filter & QDir::Executable) && [fm isExecutableFileAtPath:filename])) + if ((!(filter & QDir::Readable) && fileInfo.isReadable()) + || (!(filter & QDir::Writable) && fileInfo.isWritable()) + || (!(filter & QDir::Executable) && fileInfo.isExecutable())) return NO; } - if (!(filter & QDir::Hidden) - && (qtFileName.startsWith(u'.') || [self isHiddenFileAtURL:url])) + + // We control the visibility of hidden files via the showsHiddenFiles + // property on the panel, based on QDir::Hidden being set. But the user + // can also toggle this via the Command+Shift+. keyboard shortcut, + // in which case they have explicitly requested to show hidden files, + // and we should enable them even if QDir::Hidden was not set. In + // effect, we don't need to filter on QDir::Hidden here. + + return YES; +} + +- (BOOL)panel:(id)sender validateURL:(NSURL *)url error:(NSError * _Nullable *)outError +{ + Q_ASSERT(sender == m_panel); + + if (!m_panel.allowedFileTypes && !m_selectedNameFilter.isEmpty()) { + // The save panel hasn't done filtering on our behalf, + // either because we couldn't represent the filter via + // allowedFileTypes, or we opted out due to a multi part + // extension, so do the filtering/validation ourselves. + QFileInfo fileInfo(QString::fromNSString(url.path).normalized(QString::NormalizationForm_C)); + + if ([self fileInfoMatchesCurrentNameFilter:fileInfo]) + return YES; + + if (fileInfo.suffix().isEmpty()) { + // The filter requires a file name with an extension. + // We're going to add a default file name in selectedFiles, + // to match the native behavior. Check now that we can + // overwrite the file, if is already exists. + fileInfo = [self applyDefaultSuffixFromCurrentNameFilter:fileInfo]; + + if (!fileInfo.exists() || m_options->testOption(QFileDialogOptions::DontConfirmOverwrite)) + return YES; + + QMacAutoReleasePool pool; + auto *alert = [[NSAlert new] autorelease]; + alert.alertStyle = NSAlertStyleCritical; + + alert.messageText = [NSString stringWithFormat:qt_mac_AppKitString(@"SavePanel", + @"\\U201c%@\\U201d already exists. Do you want to replace it?"), + fileInfo.fileName().toNSString()]; + alert.informativeText = [NSString stringWithFormat:qt_mac_AppKitString(@"SavePanel", + @"A file or folder with the same name already exists in the folder %@. " + "Replacing it will overwrite its current contents."), + fileInfo.absoluteDir().dirName().toNSString()]; + + auto *replaceButton = [alert addButtonWithTitle:qt_mac_AppKitString(@"SavePanel", @"Replace")]; + replaceButton.hasDestructiveAction = YES; + replaceButton.tag = 1337; + [alert addButtonWithTitle:qt_mac_AppKitString(@"Common", @"Cancel")]; + + [alert beginSheetModalForWindow:m_panel + completionHandler:^(NSModalResponse returnCode) { + [NSApp stopModalWithCode:returnCode]; + }]; + return [NSApp runModalForWindow:alert.window] == replaceButton.tag; + } else { + QFileInfo firstFilter(m_selectedNameFilter.first()); + auto *domain = qGuiApp->organizationDomain().toNSString(); + *outError = [NSError errorWithDomain:domain code:0 userInfo:@{ + NSLocalizedDescriptionKey:[NSString stringWithFormat:qt_mac_AppKitString(@"SavePanel", + @"You cannot save this document with extension \\U201c.%1$@\\U201d at the end " + "of the name. The required extension is \\U201c.%2$@\\U201d."), + fileInfo.completeSuffix().toNSString(), firstFilter.completeSuffix().toNSString()] + }]; return NO; + } + } return YES; } +- (QFileInfo)applyDefaultSuffixFromCurrentNameFilter:(const QFileInfo &)fileInfo +{ + QFileInfo filterInfo(m_selectedNameFilter.first()); + return QFileInfo(fileInfo.absolutePath(), + fileInfo.baseName() + '.' + filterInfo.completeSuffix()); +} + +- (bool)fileInfoMatchesCurrentNameFilter:(const QFileInfo &)fileInfo +{ + // No filter means accept everything + if (m_selectedNameFilter.isEmpty()) + return true; + + // Check if the current file name filter accepts the file + for (const auto &filter : m_selectedNameFilter) { + if (QDir::match(filter, fileInfo.fileName())) + return true; + } + + return false; +} + - (void)setNameFilters:(const QStringList &)filters hideDetails:(BOOL)hideDetails { [m_popupButton removeAllItems]; - *m_nameFilterDropDownList = filters; + m_nameFilterDropDownList = filters; if (filters.size() > 0){ for (int i = 0; i < filters.size(); ++i) { - QString filter = hideDetails ? [self removeExtensions:filters.at(i)] : filters.at(i); + const QString filter = hideDetails ? [self removeExtensions:filters.at(i)] : filters.at(i); [m_popupButton.menu addItemWithTitle:filter.toNSString() action:nil keyEquivalent:@""]; } [m_popupButton selectItemAtIndex:0]; @@ -290,8 +376,8 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions; Q_UNUSED(sender); if (!m_helper) return; - QString selection = m_nameFilterDropDownList->value([m_popupButton indexOfSelectedItem]); - *m_selectedNameFilter = [self findStrippedFilterWithVisualFilterName:selection]; + const QString selection = m_nameFilterDropDownList.value([m_popupButton indexOfSelectedItem]); + m_selectedNameFilter = [self findStrippedFilterWithVisualFilterName:selection]; [m_panel validateVisibleColumns]; [self updateProperties]; @@ -310,18 +396,25 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions; } return result; } else { - QList<QUrl> result; QString filename = QString::fromNSString(m_panel.URL.path).normalized(QString::NormalizationForm_C); - const QString defaultSuffix = m_options->defaultSuffix(); - const QFileInfo fileInfo(filename); + QFileInfo fileInfo(filename); + + if (fileInfo.suffix().isEmpty() && ![self fileInfoMatchesCurrentNameFilter:fileInfo]) { + // We end up in this situation if we accept a file name without extension + // in panel:validateURL:error. If so, we match the behavior of the native + // save dialog and add the first of the accepted extension from the filter. + fileInfo = [self applyDefaultSuffixFromCurrentNameFilter:fileInfo]; + } // If neither the user or the NSSavePanel have provided a suffix, use // the default suffix (if it exists). - if (fileInfo.suffix().isEmpty() && !defaultSuffix.isEmpty()) - filename.append('.').append(defaultSuffix); + const QString defaultSuffix = m_options->defaultSuffix(); + if (fileInfo.suffix().isEmpty() && !defaultSuffix.isEmpty()) { + fileInfo.setFile(fileInfo.absolutePath(), + fileInfo.baseName() + '.' + defaultSuffix); + } - result << QUrl::fromLocalFile(filename); - return result; + return { QUrl::fromLocalFile(fileInfo.filePath()) }; } } @@ -353,19 +446,25 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions; m_panel.allowedFileTypes = [self computeAllowedFileTypes]; - // Explicitly show extensions if we detect a filter - // that has a multi-part extension. This prevents - // confusing situations where the user clicks e.g. - // 'foo.tar.gz' and 'foo.tar' is populated in the - // file name box, but when then clicking save macOS - // will warn that the file needs to end in .gz, - // due to thinking the user tried to save the file - // as a 'tar' file instead. Unfortunately this - // property can only be set before the panel is - // shown, so it will not have any effect when - // switching filters in an already opened dialog. - if (m_panel.allowedFileTypes.count > 2) - m_panel.extensionHidden = NO; + // Setting allowedFileTypes to nil is not enough to reset any + // automatically added extension based on a previous filter. + // This is problematic because extensions can in some cases + // be hidden from the user, resulting in confusion when the + // resulting file name doesn't match the current empty filter. + // We work around this by temporarily resetting the allowed + // content type to one without an extension, which forces + // the save panel to update and remove the extension. + const bool nameFieldHasExtension = m_panel.nameFieldStringValue.pathExtension.length > 0; + if (!m_panel.allowedFileTypes && !nameFieldHasExtension && !openpanel_cast(m_panel)) { + if (!UTTypeDirectory.preferredFilenameExtension) { + m_panel.allowedContentTypes = @[ UTTypeDirectory ]; + m_panel.allowedFileTypes = nil; + } else { + qWarning() << "UTTypeDirectory unexpectedly reported an extension"; + } + } + + m_panel.showsHiddenFiles = m_options->filter().testFlag(QDir::Hidden); if (m_panel.visible) [m_panel validateVisibleColumns]; @@ -378,10 +477,18 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions; if (!m_helper) return; + // Save panels only allow you to select directories, which + // means currentChanged will only be emitted when selecting + // a directory, and if so, with the latest chosen file name, + // which is confusing and inconsistent. We choose to bail + // out entirely for save panels, to give consistent behavior. + if (!openpanel_cast(m_panel)) + return; + if (m_panel.visible) { - QString selection = QString::fromNSString(m_panel.URL.path); - if (selection != *m_currentSelection) { - *m_currentSelection = selection; + const QString selection = QString::fromNSString(m_panel.URL.path); + if (selection != m_currentSelection) { + m_currentSelection = selection; emit m_helper->currentChanged(QUrl::fromLocalFile(selection)); } } @@ -394,14 +501,7 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions; if (!m_helper) return; - if (!(path && path.length) || [path isEqualToString:m_currentDirectory]) - return; - - [m_currentDirectory release]; - m_currentDirectory = [path retain]; - - // ### fixme: priv->setLastVisitedDirectory(newDir); - emit m_helper->directoryEntered(QUrl::fromLocalFile(QString::fromNSString(m_currentDirectory))); + m_helper->panelDirectoryDidChange(path); } /* @@ -409,11 +509,9 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions; for the current name filter, and updates the save panel. If a filter do not conform to the format *.xyz or * or *.*, - all files types are allowed. - - Extensions with more than one part (e.g. "tar.gz") are - reduced to their final part, as NSSavePanel does not deal - well with multi-part extensions. + or contains an extensions with more than one part (e.g. "tar.gz") + we treat that as allowing all file types, and do our own + validation in panel:validateURL:error. */ - (NSArray<NSString*>*)computeAllowedFileTypes { @@ -421,7 +519,7 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions; return nil; // panel:shouldEnableURL: does the file filtering for NSOpenPanel QStringList fileTypes; - for (const QString &filter : *m_selectedNameFilter) { + for (const QString &filter : std::as_const(m_selectedNameFilter)) { if (!filter.startsWith("*."_L1)) continue; @@ -432,6 +530,9 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions; continue; auto extensions = filter.split('.', Qt::SkipEmptyParts); + if (extensions.count() > 2) + return nil; + fileTypes += extensions.last(); } @@ -468,10 +569,10 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions; m_popupButton.target = self; m_popupButton.action = @selector(filterChanged:); - if (m_nameFilterDropDownList->size() > 0) { + if (!m_nameFilterDropDownList.isEmpty()) { int filterToUse = -1; - for (int i = 0; i < m_nameFilterDropDownList->size(); ++i) { - QString currentFilter = m_nameFilterDropDownList->at(i); + for (int i = 0; i < m_nameFilterDropDownList.size(); ++i) { + const QString currentFilter = m_nameFilterDropDownList.at(i); if (selectedFilter == currentFilter || (filterToUse == -1 && currentFilter.startsWith(selectedFilter))) filterToUse = i; @@ -485,9 +586,9 @@ typedef QSharedPointer<QFileDialogOptions> SharedPointerFileDialogOptions; - (QStringList) findStrippedFilterWithVisualFilterName:(QString)name { - for (int i = 0; i < m_nameFilterDropDownList->size(); ++i) { - if (m_nameFilterDropDownList->at(i).startsWith(name)) - return QPlatformFileDialogHelper::cleanFilterList(m_nameFilterDropDownList->at(i)); + for (const QString ¤tFilter : std::as_const(m_nameFilterDropDownList)) { + if (currentFilter.startsWith(name)) + return QPlatformFileDialogHelper::cleanFilterList(currentFilter); } return QStringList(); } @@ -528,21 +629,32 @@ void QCocoaFileDialogHelper::panelClosed(NSInteger result) void QCocoaFileDialogHelper::setDirectory(const QUrl &directory) { + m_directory = directory; + if (m_delegate) m_delegate->m_panel.directoryURL = [NSURL fileURLWithPath:directory.toLocalFile().toNSString()]; - else - m_directory = directory; } QUrl QCocoaFileDialogHelper::directory() const { - if (m_delegate) { - QString path = QString::fromNSString(m_delegate->m_panel.directoryURL.path).normalized(QString::NormalizationForm_C); - return QUrl::fromLocalFile(path); - } return m_directory; } +void QCocoaFileDialogHelper::panelDirectoryDidChange(NSString *path) +{ + if (!path || [path isEqual:NSNull.null] || !path.length) + return; + + const auto oldDirectory = m_directory; + m_directory = QUrl::fromLocalFile( + QString::fromNSString(path).normalized(QString::NormalizationForm_C)); + + if (m_directory != oldDirectory) { + // FIXME: Plumb old directory back to QFileDialog's lastVisitedDir? + emit directoryEntered(m_directory); + } +} + void QCocoaFileDialogHelper::selectFile(const QUrl &filename) { QString filePath = filename.toLocalFile(); diff --git a/src/plugins/platforms/cocoa/qcocoahelpers.h b/src/plugins/platforms/cocoa/qcocoahelpers.h index 694e57e73d..c6862a9e65 100644 --- a/src/plugins/platforms/cocoa/qcocoahelpers.h +++ b/src/plugins/platforms/cocoa/qcocoahelpers.h @@ -56,16 +56,6 @@ NSDragOperation qt_mac_mapDropActions(Qt::DropActions actions); Qt::DropAction qt_mac_mapNSDragOperation(NSDragOperation nsActions); Qt::DropActions qt_mac_mapNSDragOperations(NSDragOperation nsActions); -template <typename T> -typename std::enable_if<std::is_pointer<T>::value, T>::type -qt_objc_cast(id object) -{ - if ([object isKindOfClass:[typename std::remove_pointer<T>::type class]]) - return static_cast<T>(object); - - return nil; -} - QT_MANGLE_NAMESPACE(QNSView) *qnsview_cast(NSView *view); // Misc @@ -88,6 +78,9 @@ Qt::MouseButtons currentlyPressedMouseButtons(); // accelerators. QString qt_mac_removeAmpersandEscapes(QString s); +// Similar to __NXKitString for localized AppKit strings +NSString *qt_mac_AppKitString(NSString *table, NSString *key); + enum { QtCocoaEventSubTypeWakeup = SHRT_MAX, QtCocoaEventSubTypePostMessage = SHRT_MAX-1 diff --git a/src/plugins/platforms/cocoa/qcocoahelpers.mm b/src/plugins/platforms/cocoa/qcocoahelpers.mm index ec64c94d0b..1eba88d5e3 100644 --- a/src/plugins/platforms/cocoa/qcocoahelpers.mm +++ b/src/plugins/platforms/cocoa/qcocoahelpers.mm @@ -336,6 +336,15 @@ QString qt_mac_removeAmpersandEscapes(QString s) return QPlatformTheme::removeMnemonics(s).trimmed(); } +NSString *qt_mac_AppKitString(NSString *table, NSString *key) +{ + static const NSBundle *appKit = [NSBundle bundleForClass:NSApplication.class]; + if (!appKit) + return key; + + return [appKit localizedStringForKey:key value:nil table:table]; +} + QT_END_NAMESPACE /*! \internal diff --git a/src/plugins/platforms/cocoa/qcocoainputcontext.mm b/src/plugins/platforms/cocoa/qcocoainputcontext.mm index b242cd69c6..70461376e2 100644 --- a/src/plugins/platforms/cocoa/qcocoainputcontext.mm +++ b/src/plugins/platforms/cocoa/qcocoainputcontext.mm @@ -150,11 +150,18 @@ void QCocoaInputContext::updateLocale() QString language = QString::fromNSString(languages.firstObject); QLocale locale(language); - if (m_locale != locale) { + + bool localeUpdated = m_locale != locale; + static bool firstUpdate = true; + + m_locale = locale; + + if (localeUpdated && !firstUpdate) { qCDebug(lcQpaInputMethods) << "Reporting new locale" << locale; - m_locale = locale; emitLocaleChanged(); } + + firstUpdate = false; } QT_END_NAMESPACE diff --git a/src/plugins/platforms/cocoa/qcocoaintegration.h b/src/plugins/platforms/cocoa/qcocoaintegration.h index 09904efbaf..664700cf51 100644 --- a/src/plugins/platforms/cocoa/qcocoaintegration.h +++ b/src/plugins/platforms/cocoa/qcocoaintegration.h @@ -84,8 +84,7 @@ public: QCocoaServices *services() const override; QVariant styleHint(StyleHint hint) const override; - Qt::KeyboardModifiers queryKeyboardModifiers() const override; - QList<int> possibleKeys(const QKeyEvent *event) const override; + QPlatformKeyMapper *keyMapper() const override; void setApplicationIcon(const QIcon &icon) const override; void setApplicationBadge(qint64 number) override; diff --git a/src/plugins/platforms/cocoa/qcocoaintegration.mm b/src/plugins/platforms/cocoa/qcocoaintegration.mm index 56c65a0836..2ce39ff897 100644 --- a/src/plugins/platforms/cocoa/qcocoaintegration.mm +++ b/src/plugins/platforms/cocoa/qcocoaintegration.mm @@ -40,6 +40,7 @@ #include <QtGui/private/qfontengine_coretext_p.h> #include <IOKit/graphics/IOGraphicsLib.h> +#include <UniformTypeIdentifiers/UTCoreTypes.h> #include <inttypes.h> @@ -124,9 +125,9 @@ QCocoaIntegration::QCocoaIntegration(const QStringList ¶mList) #endif mFontDb.reset(new QCoreTextFontDatabaseEngineFactory<QCoreTextFontEngine>); - QString icStr = QPlatformInputContextFactory::requested(); - icStr.isNull() ? mInputContext.reset(new QCocoaInputContext) - : mInputContext.reset(QPlatformInputContextFactory::create(icStr)); + auto icStrs = QPlatformInputContextFactory::requested(); + icStrs.isEmpty() ? mInputContext.reset(new QCocoaInputContext) + : mInputContext.reset(QPlatformInputContextFactory::create(icStrs)); initResources(); QMacAutoReleasePool pool; @@ -141,16 +142,6 @@ QCocoaIntegration::QCocoaIntegration(const QStringList ¶mList) // wants to be foreground applications so change the process type. (But // see the function implementation for exceptions.) qt_mac_transformProccessToForegroundApplication(); - - // Move the application window to front to make it take focus, also when launching - // from the terminal. On 10.12+ this call has been moved to applicationDidFinishLauching - // to work around issues with loss of focus at startup. - if (QOperatingSystemVersion::current() < QOperatingSystemVersion::MacOSSierra) { - // Ignoring other apps is necessary (we must ignore the terminal), but makes - // Qt apps play slightly less nice with other apps when lanching from Finder - // (See the activateIgnoringOtherApps docs.) - [cocoaApplication activateIgnoringOtherApps : YES]; - } } // Qt 4 also does not set the application delegate, so that behavior @@ -194,6 +185,9 @@ QCocoaIntegration::~QCocoaIntegration() [[NSApplication sharedApplication] setDelegate:nil]; } + // Stop global mouse event and app activation monitoring + QCocoaWindow::removePopupMonitor(); + #ifndef QT_NO_CLIPBOARD // Delete the clipboard integration and destroy mime type converters. // Deleting the clipboard integration flushes promised pastes using @@ -242,6 +236,7 @@ bool QCocoaIntegration::hasCapability(QPlatformIntegration::Capability cap) cons case RasterGLSurface: case ApplicationState: case ApplicationIcon: + case BackingStoreStaticContents: return true; default: return QPlatformIntegration::hasCapability(cap); @@ -309,6 +304,18 @@ QPlatformBackingStore *QCocoaIntegration::createPlatformBackingStore(QWindow *wi return new QCALayerBackingStore(window); case QSurface::MetalSurface: case QSurface::OpenGLSurface: + case QSurface::VulkanSurface: + // If the window is a widget window, we know that the QWidgetRepaintManager + // will explicitly use rhiFlush() for the window owning the backingstore, + // and any child window with the same surface format. This means we can + // safely return a QCALayerBackingStore here, to ensure that any plain + // flush() for child windows that don't have a matching surface format + // will still work, by setting the layer's contents property. + if (window->inherits("QWidgetWindow")) + return new QCALayerBackingStore(window); + + // Otherwise we return a QRhiBackingStore, that implements flush() in + // terms of rhiFlush(). return new QRhiBackingStore(window); default: return nullptr; @@ -399,14 +406,9 @@ QVariant QCocoaIntegration::styleHint(StyleHint hint) const return QPlatformIntegration::styleHint(hint); } -Qt::KeyboardModifiers QCocoaIntegration::queryKeyboardModifiers() const -{ - return QAppleKeyMapper::queryKeyboardModifiers(); -} - -QList<int> QCocoaIntegration::possibleKeys(const QKeyEvent *event) const +QPlatformKeyMapper *QCocoaIntegration::keyMapper() const { - return mKeyboardMapper->possibleKeys(event); + return mKeyboardMapper.data(); } void QCocoaIntegration::setApplicationIcon(const QIcon &icon) const @@ -439,8 +441,8 @@ void QCocoaIntegration::focusWindowChanged(QWindow *focusWindow) return; static bool hasDefaultApplicationIcon = [](){ - NSImage *genericApplicationIcon = [[NSWorkspace sharedWorkspace] - iconForFileType:NSFileTypeForHFSTypeCode(kGenericApplicationIcon)]; + NSImage *genericApplicationIcon = [NSWorkspace.sharedWorkspace + iconForContentType:UTTypeApplicationBundle]; NSImage *applicationIcon = [NSImage imageNamed:NSImageNameApplicationIcon]; NSRect rect = NSMakeRect(0, 0, 32, 32); diff --git a/src/plugins/platforms/cocoa/qcocoamenu.mm b/src/plugins/platforms/cocoa/qcocoamenu.mm index 0f39246a43..fa88a19d45 100644 --- a/src/plugins/platforms/cocoa/qcocoamenu.mm +++ b/src/plugins/platforms/cocoa/qcocoamenu.mm @@ -19,6 +19,7 @@ #include "qcocoaapplicationdelegate.h" #include <QtCore/private/qcore_mac_p.h> +#include <QtCore/qpointer.h> QT_BEGIN_NAMESPACE @@ -42,6 +43,8 @@ QCocoaMenu::~QCocoaMenu() item->setMenuParent(nullptr); } + if (isOpen()) + dismiss(); [m_nativeMenu release]; } @@ -60,7 +63,7 @@ void QCocoaMenu::setMinimumWidth(int width) void QCocoaMenu::setFont(const QFont &font) { if (font.resolveMask()) { - NSFont *customMenuFont = [NSFont fontWithName:font.families().first().toNSString() + NSFont *customMenuFont = [NSFont fontWithName:font.families().constFirst().toNSString() size:font.pointSize()]; m_nativeMenu.font = customMenuFont; } @@ -320,8 +323,12 @@ void QCocoaMenu::showPopup(const QWindow *parentWindow, const QRect &targetRect, { QMacAutoReleasePool pool; + QPointer<QCocoaMenu> guard = this; + QPoint pos = QPoint(targetRect.left(), targetRect.top() + targetRect.height()); - QCocoaWindow *cocoaWindow = parentWindow ? static_cast<QCocoaWindow *>(parentWindow->handle()) : nullptr; + // If the app quits while the menu is open (e.g. through a timer that starts before the menu was opened), + // then the window will have been destroyed before this function finishes executing. Account for that with QPointer. + QPointer<QCocoaWindow> cocoaWindow = parentWindow ? static_cast<QCocoaWindow *>(parentWindow->handle()) : nullptr; NSView *view = cocoaWindow ? cocoaWindow->view() : nil; NSMenuItem *nsItem = item ? ((QCocoaMenuItem *)item)->nsItem() : nil; @@ -404,6 +411,11 @@ void QCocoaMenu::showPopup(const QWindow *parentWindow, const QRect &targetRect, } } + if (!guard) { + menuParentGuard.dismiss(); + return; + } + // 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 && !cocoaWindow->isForeignWindow()) @@ -483,6 +495,10 @@ void QCocoaMenu::setAttachedItem(NSMenuItem *item) if (m_attachedItem) m_attachedItem.submenu = m_nativeMenu; + // NSMenuItems with a submenu and submenuAction: as the item's action + // will not take part in NSMenuValidation, so explicitly enable/disable + // the item here. See also QCocoaMenuItem::resolveTargetAction() + m_attachedItem.enabled = m_attachedItem.hasSubmenu; } NSMenuItem *QCocoaMenu::attachedItem() const diff --git a/src/plugins/platforms/cocoa/qcocoamenubar.h b/src/plugins/platforms/cocoa/qcocoamenubar.h index f450c507b5..785de9c0f6 100644 --- a/src/plugins/platforms/cocoa/qcocoamenubar.h +++ b/src/plugins/platforms/cocoa/qcocoamenubar.h @@ -9,6 +9,8 @@ #include <qpa/qplatformmenu.h> #include "qcocoamenu.h" +#include <QtCore/qpointer.h> + QT_BEGIN_NAMESPACE class QCocoaWindow; diff --git a/src/plugins/platforms/cocoa/qcocoamenubar.mm b/src/plugins/platforms/cocoa/qcocoamenubar.mm index 7cc57055c7..2493d90724 100644 --- a/src/plugins/platforms/cocoa/qcocoamenubar.mm +++ b/src/plugins/platforms/cocoa/qcocoamenubar.mm @@ -36,7 +36,7 @@ QCocoaMenuBar::QCocoaMenuBar() QCocoaMenuBar::~QCocoaMenuBar() { - qCDebug(lcQpaMenus) << "Destructing" << this << "with" << m_nativeMenu;; + qCDebug(lcQpaMenus) << "Destructing" << this << "with" << m_nativeMenu; for (auto menu : std::as_const(m_menus)) { if (!menu) continue; @@ -204,8 +204,7 @@ void QCocoaMenuBar::syncMenu_helper(QPlatformMenu *menu, bool menubarUpdate) // and document that the user needs to ensure their application matches // this translation. if ([menuTitle isEqual:@"Edit"] || [menuTitle isEqual:tr("Edit").toNSString()]) { - static const NSBundle *appKit = [NSBundle bundleForClass:NSApplication.class]; - menuItem.title = [appKit localizedStringForKey:@"Edit" value:menuTitle table:@"InputManager"]; + menuItem.title = qt_mac_AppKitString(@"InputManager", @"Edit"); } else { // The Edit menu is the only case we know of so far, but to be on // the safe side we always sync the menu title. @@ -327,7 +326,21 @@ void QCocoaMenuBar::updateMenuBarImmediately() } [mergedItems release]; - [NSApp setMainMenu:mb->nsMenu()]; + + NSMenu *newMainMenu = mb->nsMenu(); + if (NSApp.mainMenu == newMainMenu) { + // NSApplication triggers _customizeMainMenu when the menu + // changes, which takes care of adding text input items to + // the edit menu e.g., but this doesn't happen if the menu + // is the same. In our case we might be re-using an existing + // menu, but the menu might have new sub menus that need to + // be customized. To ensure NSApplication does the right + // thing we reset the main menu first. + qCDebug(lcQpaMenus) << "Clearing main menu temporarily"; + NSApp.mainMenu = nil; + } + NSApp.mainMenu = newMainMenu; + insertWindowMenu(); [loader qtTranslateApplicationMenu]; } @@ -348,6 +361,15 @@ void QCocoaMenuBar::insertWindowMenu() winMenuItem.hidden = YES; winMenuItem.submenu = [[[NSMenu alloc] initWithTitle:@"QtWindowMenu"] autorelease]; + + // AppKit has a bug in [NSApplication setWindowsMenu:] where it will resolve + // the last item of the window menu's itemArray, but not account for the array + // being empty, resulting in a lookup of itemAtIndex:-1. To work around this, + // we insert a hidden dummy item into the menu. See FB13369198. + auto *dummyItem = [[NSMenuItem alloc] initWithTitle:@"" action:nil keyEquivalent:@""]; + dummyItem.hidden = YES; + [winMenuItem.submenu addItem:[dummyItem autorelease]]; + [mainMenu insertItem:winMenuItem atIndex:mainMenu.itemArray.count]; app.windowsMenu = winMenuItem.submenu; diff --git a/src/plugins/platforms/cocoa/qcocoamenuitem.h b/src/plugins/platforms/cocoa/qcocoamenuitem.h index e438fd2a07..f677ffb7a7 100644 --- a/src/plugins/platforms/cocoa/qcocoamenuitem.h +++ b/src/plugins/platforms/cocoa/qcocoamenuitem.h @@ -8,6 +8,8 @@ #include <qpa/qplatformmenu.h> #include <QtGui/QImage> +#include <QtCore/qpointer.h> + Q_FORWARD_DECLARE_OBJC_CLASS(NSMenuItem); Q_FORWARD_DECLARE_OBJC_CLASS(NSMenu); Q_FORWARD_DECLARE_OBJC_CLASS(NSObject); diff --git a/src/plugins/platforms/cocoa/qcocoamenuitem.mm b/src/plugins/platforms/cocoa/qcocoamenuitem.mm index 0acae8d679..3a0f71bc50 100644 --- a/src/plugins/platforms/cocoa/qcocoamenuitem.mm +++ b/src/plugins/platforms/cocoa/qcocoamenuitem.mm @@ -473,7 +473,20 @@ void QCocoaMenuItem::resolveTargetAction() roleAction = @selector(selectAll:); break; default: - roleAction = @selector(qt_itemFired:); + if (m_menu) { + // Menu items that represent sub menus should have submenuAction: as their + // action, so that clicking the menu item opens the sub menu without closing + // the entire menu hierarchy. A menu item with this action and a valid submenu + // will disable NSMenuValidation for the item, which is normally not an issue + // as NSMenuItems are enabled by default. But in our case, we haven't attached + // the submenu yet, which results in AppKit concluding that there's no validator + // for the item (the target is nil, and nothing responds to submenuAction:), and + // will in response disable the menu item. To work around this we explicitly + // enable the menu item in QCocoaMenu::setAttachedItem() once we have a submenu. + roleAction = @selector(submenuAction:); + } else { + roleAction = @selector(qt_itemFired:); + } } m_native.action = roleAction; diff --git a/src/plugins/platforms/cocoa/qcocoamessagedialog.h b/src/plugins/platforms/cocoa/qcocoamessagedialog.h index 564dd915c5..b8c273469a 100644 --- a/src/plugins/platforms/cocoa/qcocoamessagedialog.h +++ b/src/plugins/platforms/cocoa/qcocoamessagedialog.h @@ -28,6 +28,7 @@ private: Qt::WindowModality modality() const; NSAlert *m_alert = nullptr; QEventLoop *m_eventLoop = nullptr; + NSModalResponse runModal() const; void processResponse(NSModalResponse response); }; diff --git a/src/plugins/platforms/cocoa/qcocoamessagedialog.mm b/src/plugins/platforms/cocoa/qcocoamessagedialog.mm index e058450ebd..84525099c9 100644 --- a/src/plugins/platforms/cocoa/qcocoamessagedialog.mm +++ b/src/plugins/platforms/cocoa/qcocoamessagedialog.mm @@ -91,15 +91,25 @@ bool QCocoaMessageDialog::show(Qt::WindowFlags windowFlags, Qt::WindowModality w if (!options()) return false; + // NSAlert doesn't have a section for detailed text + if (!options()->detailedText().isEmpty()) { + qCWarning(lcQpaDialogs, "Message box contains detailed text"); + return false; + } + + if (Qt::mightBeRichText(options()->text()) || + Qt::mightBeRichText(options()->informativeText())) { + // Let's fallback to non-native message box, + // we only have plain NSString/text in NSAlert. + qCDebug(lcQpaDialogs, "Message box contains text in rich text format"); + return false; + } Q_ASSERT(!m_alert); m_alert = [NSAlert new]; m_alert.window.title = options()->windowTitle().toNSString(); - QString text = toPlainText(options()->text()); - QString details = toPlainText(options()->detailedText()); - if (!details.isEmpty()) - text += u"\n\n"_s + details; + const QString text = toPlainText(options()->text()); m_alert.messageText = text.toNSString(); m_alert.informativeText = toPlainText(options()->informativeText()).toNSString(); @@ -127,8 +137,8 @@ bool QCocoaMessageDialog::show(Qt::WindowFlags windowFlags, Qt::WindowModality w break; } - bool defaultButtonAdded = false; - bool cancelButtonAdded = false; + auto defaultButton = options()->defaultButton(); + auto escapeButton = options()->escapeButton(); const auto addButton = [&](auto title, auto tag, auto role) { title = QPlatformTheme::removeMnemonics(title); @@ -138,17 +148,27 @@ bool QCocoaMessageDialog::show(Qt::WindowFlags windowFlags, Qt::WindowModality w // and going toward the left/bottom. By default, the first button has a key equivalent of // Return, any button with a title of "Cancel" has a key equivalent of Escape, and any button // with the title "Don't Save" has a key equivalent of Command-D (but only if it's not the first - // button). Unfortunately QMessageBox does not currently plumb setDefaultButton/setEscapeButton - // through the dialog options, so we can't forward this information directly. The closest we - // can get right now is to use the role to set the button's key equivalent. + // button). If an explicit default or escape button has been set, we respect these, + // and otherwise we fall back to role-based default and escape buttons. + + qCDebug(lcQpaDialogs).verbosity(0) << "Adding button" << title << "with" << role; - if (role == AcceptRole && !defaultButtonAdded) { + if (!defaultButton && role == AcceptRole) + defaultButton = tag; + + if (tag == defaultButton) button.keyEquivalent = @"\r"; - defaultButtonAdded = true; - } else if (role == RejectRole && !cancelButtonAdded) { + else if ([button.keyEquivalent isEqualToString:@"\r"]) + button.keyEquivalent = @""; + + if (!escapeButton && role == RejectRole) + escapeButton = tag; + + // Don't override default button with escape button, to match AppKit default + if (tag == escapeButton && ![button.keyEquivalent isEqualToString:@"\r"]) button.keyEquivalent = @"\e"; - cancelButtonAdded = true; - } + else if ([button.keyEquivalent isEqualToString:@"\e"]) + button.keyEquivalent = @""; if (@available(macOS 11, *)) button.hasDestructiveAction = role == DestructiveRole; @@ -168,30 +188,69 @@ bool QCocoaMessageDialog::show(Qt::WindowFlags windowFlags, Qt::WindowModality w button.tag = tag; }; + // Resolve all dialog buttons from the options, both standard and custom + + struct Button { QString title; int identifier; ButtonRole role; }; + std::vector<Button> buttons; + const auto *platformTheme = QGuiApplicationPrivate::platformTheme(); if (auto standardButtons = options()->standardButtons()) { - for (int standardButton = FirstButton; standardButton < LastButton; standardButton <<= 1) { + for (int standardButton = FirstButton; standardButton <= LastButton; standardButton <<= 1) { if (standardButtons & standardButton) { auto title = platformTheme->standardButtonText(standardButton); - addButton(title, standardButton, buttonRole(StandardButton(standardButton))); + buttons.push_back({ + title, standardButton, buttonRole(StandardButton(standardButton)) + }); } } } - const auto customButtons = options()->customButtons(); for (auto customButton : customButtons) - addButton(customButton.label, customButton.id, customButton.role); + buttons.push_back({customButton.label, customButton.id, customButton.role}); + + // Sort them according to the QPlatformDialogHelper::ButtonLayout for macOS + + // The ButtonLayout adds one additional role, AlternateRole, which is used + // for any AcceptRole beyond the first one, and should be ordered before the + // AcceptRole. Set this up by fixing the roles up front. + bool seenAccept = false; + for (auto &button : buttons) { + if (button.role == AcceptRole) { + if (!seenAccept) + seenAccept = true; + else + button.role = AlternateRole; + } + } + + std::vector<Button> orderedButtons; + const int *layoutEntry = buttonLayout(Qt::Horizontal, ButtonLayout::MacLayout); + while (*layoutEntry != QPlatformDialogHelper::EOL) { + const auto role = ButtonRole(*layoutEntry & ~ButtonRole::Reverse); + const bool reverse = *layoutEntry & ButtonRole::Reverse; + + auto addButton = [&](const Button &button) { + if (button.role == role) + orderedButtons.push_back(button); + }; + if (reverse) + std::for_each(std::crbegin(buttons), std::crend(buttons), addButton); + else + std::for_each(std::cbegin(buttons), std::cend(buttons), addButton); - // QMessageDialog's logic for adding a fallback OK button if no other buttons - // are added depends on QMessageBox::showEvent(), which is too late when - // native dialogs are in use. To ensure there's always an OK button with a tag - // we recognize we add it explicitly here as a fallback. - if (!m_alert.buttons.count) { - addButton(platformTheme->standardButtonText(StandardButton::Ok), - StandardButton::Ok, ButtonRole::AcceptRole); + ++layoutEntry; } + // Add them to the alert in reverse order, since buttons are added right to left + for (auto button = orderedButtons.crbegin(); button != orderedButtons.crend(); ++button) + addButton(button->title, button->identifier, button->role); + + // If we didn't find a an explicit or implicit default button above + // we restore the AppKit behavior of making the first button default. + if (!defaultButton) + m_alert.buttons.firstObject.keyEquivalent = @"\r"; + if (auto checkBoxLabel = options()->checkBoxLabel(); !checkBoxLabel.isNull()) { checkBoxLabel = QPlatformTheme::removeMnemonics(checkBoxLabel); m_alert.suppressionButton.title = checkBoxLabel.toNSString(); @@ -219,10 +278,10 @@ bool QCocoaMessageDialog::show(Qt::WindowFlags windowFlags, Qt::WindowModality w // but also make sure that if the user returns to the main runloop // we'll run the modal dialog from there. QTimer::singleShot(0, this, [this]{ - if (m_alert && NSApp.modalWindow != m_alert.window) { + if (m_alert && !m_alert.window.visible) { qCDebug(lcQpaDialogs) << "Running deferred modal" << m_alert; QCocoaEventDispatcher::clearCurrentThreadCocoaEventDispatcherInterruptFlag(); - processResponse([m_alert runModal]); + processResponse(runModal()); } }); } @@ -230,6 +289,20 @@ bool QCocoaMessageDialog::show(Qt::WindowFlags windowFlags, Qt::WindowModality w return true; } +// We shouldn't get NSModalResponseContinue as a response from NSAlert::runModal, +// and processResponse must not be called with that value (if we are there, it's +// too late to do anything about it. +// However, as QTBUG-114546 shows, there are scenarios where we might get that +// response anyway. We interpret it to keep the modal loop running, and we only +// return if we got something else to pass to processResponse. +NSModalResponse QCocoaMessageDialog::runModal() const +{ + NSModalResponse response = NSModalResponseContinue; + while (response == NSModalResponseContinue) + response = [m_alert runModal]; + return response; +} + void QCocoaMessageDialog::exec() { Q_ASSERT(m_alert); @@ -242,7 +315,7 @@ void QCocoaMessageDialog::exec() } else { qCDebug(lcQpaDialogs) << "Running modal" << m_alert; QCocoaEventDispatcher::clearCurrentThreadCocoaEventDispatcherInterruptFlag(); - processResponse([m_alert runModal]); + processResponse(runModal()); } } diff --git a/src/plugins/platforms/cocoa/qcocoansmenu.mm b/src/plugins/platforms/cocoa/qcocoansmenu.mm index 4bc9b0b5f9..ba222a3ef4 100644 --- a/src/plugins/platforms/cocoa/qcocoansmenu.mm +++ b/src/plugins/platforms/cocoa/qcocoansmenu.mm @@ -16,6 +16,8 @@ #include <QtCore/qvarlengtharray.h> #include <QtGui/private/qapplekeymapper_p.h> +#include <QtCore/qpointer.h> + static NSString *qt_mac_removePrivateUnicode(NSString *string) { if (const int len = string.length) { diff --git a/src/plugins/platforms/cocoa/qcocoascreen.h b/src/plugins/platforms/cocoa/qcocoascreen.h index 267da96433..7708dc1968 100644 --- a/src/plugins/platforms/cocoa/qcocoascreen.h +++ b/src/plugins/platforms/cocoa/qcocoascreen.h @@ -41,6 +41,7 @@ public: QWindow *topLevelAt(const QPoint &point) const override; QList<QPlatformScreen *> virtualSiblings() const override; QPlatformScreen::SubpixelAntialiasingType subpixelAntialiasingTypeHint() const override; + Qt::ScreenOrientation orientation() const override; // ---------------------------------------------------- @@ -90,6 +91,7 @@ private: QSizeF m_physicalSize; QCocoaCursor *m_cursor; qreal m_devicePixelRatio = 0; + qreal m_rotation = 0; CVDisplayLinkRef m_displayLink = nullptr; dispatch_source_t m_displayLinkSource = nullptr; diff --git a/src/plugins/platforms/cocoa/qcocoascreen.mm b/src/plugins/platforms/cocoa/qcocoascreen.mm index f12f3f8ecc..2249658189 100644 --- a/src/plugins/platforms/cocoa/qcocoascreen.mm +++ b/src/plugins/platforms/cocoa/qcocoascreen.mm @@ -15,6 +15,7 @@ #include <IOKit/graphics/IOGraphicsLib.h> #include <QtGui/private/qwindow_p.h> +#include <QtGui/private/qhighdpiscaling_p.h> #include <QtCore/private/qcore_mac_p.h> #include <QtCore/private/qeventdispatcher_cf_p.h> @@ -199,7 +200,7 @@ QCocoaScreen::~QCocoaScreen() static QString displayName(CGDirectDisplayID displayID) { QIOType<io_iterator_t> iterator; - if (IOServiceGetMatchingServices(kIOMasterPortDefault, + if (IOServiceGetMatchingServices(kIOMainPortDefault, IOServiceMatching("IODisplayConnect"), &iterator)) return QString(); @@ -247,6 +248,7 @@ void QCocoaScreen::update(CGDirectDisplayID displayId) const QRect previousGeometry = m_geometry; const QRect previousAvailableGeometry = m_availableGeometry; const qreal previousRefreshRate = m_refreshRate; + const double previousRotation = m_rotation; // The reference screen for the geometry is always the primary screen QRectF primaryScreenGeometry = QRectF::fromCGRect(CGDisplayBounds(CGMainDisplayID())); @@ -271,6 +273,7 @@ void QCocoaScreen::update(CGDirectDisplayID displayId) QCFType<CGDisplayModeRef> displayMode = CGDisplayCopyDisplayMode(m_displayId); float refresh = CGDisplayModeGetRefreshRate(displayMode); m_refreshRate = refresh > 0 ? refresh : 60.0; + m_rotation = CGDisplayRotation(displayId); if (@available(macOS 10.15, *)) m_name = QString::fromNSString(nsScreen.localizedName); @@ -279,6 +282,9 @@ void QCocoaScreen::update(CGDirectDisplayID displayId) const bool didChangeGeometry = m_geometry != previousGeometry || m_availableGeometry != previousAvailableGeometry; + if (m_rotation != previousRotation) + QWindowSystemInterface::handleScreenOrientationChange(screen(), orientation()); + if (didChangeGeometry) QWindowSystemInterface::handleScreenGeometryChange(screen(), geometry(), availableGeometry()); if (m_refreshRate != previousRefreshRate) @@ -465,25 +471,6 @@ void QCocoaScreen::deliverUpdateRequests() if (!platformWindow->updatesWithDisplayLink()) continue; - // QTBUG-107198: Skip updates in a live resize for a better resize experience. - if (platformWindow->isContentView() && platformWindow->view().inLiveResize) { - const QSurface::SurfaceType surfaceType = window->surfaceType(); - const bool usesMetalLayer = surfaceType == QWindow::MetalSurface || surfaceType == QWindow::VulkanSurface; - const bool usesNonDefaultContentsPlacement = [platformWindow->view() layerContentsPlacement] - != NSViewLayerContentsPlacementScaleAxesIndependently; - if (usesMetalLayer && usesNonDefaultContentsPlacement) { - static bool deliverDisplayLinkUpdatesDuringLiveResize = - qEnvironmentVariableIsSet("QT_MAC_DISPLAY_LINK_UPDATE_IN_RESIZE"); - if (!deliverDisplayLinkUpdatesDuringLiveResize) { - // Must keep the link running, we do not know what the event - // handlers for UpdateRequest (which is not sent now) would do, - // would they trigger a new requestUpdate() or not. - pauseUpdates = false; - continue; - } - } - } - platformWindow->deliverUpdateRequest(); // Another update request was triggered, keep the display link running @@ -521,41 +508,56 @@ QPlatformScreen::SubpixelAntialiasingType QCocoaScreen::subpixelAntialiasingType return type; } +Qt::ScreenOrientation QCocoaScreen::orientation() const +{ + if (m_rotation == 0) + return Qt::LandscapeOrientation; + if (m_rotation == 90) + return Qt::PortraitOrientation; + if (m_rotation == 180) + return Qt::InvertedLandscapeOrientation; + if (m_rotation == 270) + return Qt::InvertedPortraitOrientation; + return QPlatformScreen::orientation(); +} + QWindow *QCocoaScreen::topLevelAt(const QPoint &point) const { - NSPoint screenPoint = mapToNative(point); - - // Search (hit test) for the top-level window. [NSWidow windowNumberAtPoint: - // belowWindowWithWindowNumber] may return windows that are not interesting - // to Qt. The search iterates until a suitable window or no window is found. - NSInteger topWindowNumber = 0; - QWindow *window = nullptr; - do { - // Get the top-most window, below any previously rejected window. - topWindowNumber = [NSWindow windowNumberAtPoint:screenPoint - belowWindowWithWindowNumber:topWindowNumber]; - - // Continue the search if the window does not belong to this process. - NSWindow *nsWindow = [NSApp windowWithWindowNumber:topWindowNumber]; - if (!nsWindow) - continue; + __block QWindow *window = nullptr; + [NSApp enumerateWindowsWithOptions:NSWindowListOrderedFrontToBack + usingBlock:^(NSWindow *nsWindow, BOOL *stop) { + if (!nsWindow) + return; - // Continue the search if the window does not belong to Qt. - if (![nsWindow conformsToProtocol:@protocol(QNSWindowProtocol)]) - continue; + // Continue the search if the window does not belong to Qt + if (![nsWindow conformsToProtocol:@protocol(QNSWindowProtocol)]) + return; - QCocoaWindow *cocoaWindow = qnsview_cast(nsWindow.contentView).platformWindow; - if (!cocoaWindow) - continue; - window = cocoaWindow->window(); + QCocoaWindow *cocoaWindow = qnsview_cast(nsWindow.contentView).platformWindow; + if (!cocoaWindow) + return; + + QWindow *w = cocoaWindow->window(); + if (!w->isVisible()) + return; + + auto nativeGeometry = QHighDpi::toNativePixels(w->geometry(), w); + if (!nativeGeometry.contains(point)) + return; - // Continue the search if the window is not a top-level window. - if (!window->isTopLevel()) - continue; + QRegion mask = QHighDpi::toNativeLocalPosition(w->mask(), w); + if (!mask.isEmpty() && !mask.contains(point - nativeGeometry.topLeft())) + return; - // Stop searching. The current window is the correct window. - break; - } while (topWindowNumber > 0); + window = w; + + // Continue the search if the window is not a top-level window + if (!window->isTopLevel()) + return; + + *stop = true; + } + ]; return window; } diff --git a/src/plugins/platforms/cocoa/qcocoaservices.h b/src/plugins/platforms/cocoa/qcocoaservices.h index 20d9b67760..b6299570e8 100644 --- a/src/plugins/platforms/cocoa/qcocoaservices.h +++ b/src/plugins/platforms/cocoa/qcocoaservices.h @@ -4,6 +4,8 @@ #ifndef QCOCOADESKTOPSERVICES_H #define QCOCOADESKTOPSERVICES_H +#include <QtCore/qurl.h> + #include <qpa/qplatformservices.h> QT_BEGIN_NAMESPACE @@ -11,8 +13,16 @@ QT_BEGIN_NAMESPACE class QCocoaServices : public QPlatformServices { public: + bool hasCapability(Capability capability) const override; + bool openUrl(const QUrl &url) override; bool openDocument(const QUrl &url) override; + bool handleUrl(const QUrl &url); + + QPlatformServiceColorPicker *colorPicker(QWindow *parent) override; + +private: + QUrl m_handlingUrl; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/cocoa/qcocoaservices.mm b/src/plugins/platforms/cocoa/qcocoaservices.mm index 68daa660ef..87212c265c 100644 --- a/src/plugins/platforms/cocoa/qcocoaservices.mm +++ b/src/plugins/platforms/cocoa/qcocoaservices.mm @@ -4,14 +4,23 @@ #include "qcocoaservices.h" #include <AppKit/NSWorkspace.h> +#include <AppKit/NSColorSampler.h> #include <Foundation/NSURL.h> #include <QtCore/QUrl> +#include <QtCore/qscopedvaluerollback.h> + +#include <QtGui/qdesktopservices.h> +#include <QtGui/private/qcoregraphics_p.h> QT_BEGIN_NAMESPACE bool QCocoaServices::openUrl(const QUrl &url) { + // avoid recursing back into self + if (url == m_handlingUrl) + return false; + return [[NSWorkspace sharedWorkspace] openURL:url.toNSURL()]; } @@ -20,4 +29,45 @@ bool QCocoaServices::openDocument(const QUrl &url) return openUrl(url); } +/* Callback from macOS that the application should handle a URL */ +bool QCocoaServices::handleUrl(const QUrl &url) +{ + QScopedValueRollback<QUrl> rollback(m_handlingUrl, url); + // FIXME: Add platform services callback from QDesktopServices::setUrlHandler + // so that we can warn the user if calling setUrlHandler without also setting + // up the matching keys in the Info.plist file (CFBundleURLTypes and friends). + return QDesktopServices::openUrl(url); +} + +class QCocoaColorPicker : public QPlatformServiceColorPicker +{ +public: + QCocoaColorPicker() : m_colorSampler([NSColorSampler new]) {} + ~QCocoaColorPicker() { [m_colorSampler release]; } + + void pickColor() override + { + [m_colorSampler showSamplerWithSelectionHandler:^(NSColor *selectedColor) { + emit colorPicked(qt_mac_toQColor(selectedColor)); + }]; + } +private: + NSColorSampler *m_colorSampler = nullptr; +}; + + +QPlatformServiceColorPicker *QCocoaServices::colorPicker(QWindow *parent) +{ + Q_UNUSED(parent); + return new QCocoaColorPicker; +} + +bool QCocoaServices::hasCapability(Capability capability) const +{ + switch (capability) { + case ColorPicking: return true; + default: return false; + } +} + QT_END_NAMESPACE diff --git a/src/plugins/platforms/cocoa/qcocoasystemtrayicon.mm b/src/plugins/platforms/cocoa/qcocoasystemtrayicon.mm index ec366f5483..cec8301cf6 100644 --- a/src/plugins/platforms/cocoa/qcocoasystemtrayicon.mm +++ b/src/plugins/platforms/cocoa/qcocoasystemtrayicon.mm @@ -184,7 +184,18 @@ void QCocoaSystemTrayIcon::updateIcon(const QIcon &icon) void QCocoaSystemTrayIcon::updateMenu(QPlatformMenu *menu) { - m_statusItem.menu = menu ? static_cast<QCocoaMenu *>(menu)->nsMenu() : nil; + auto *nsMenu = menu ? static_cast<QCocoaMenu *>(menu)->nsMenu() : nil; + if (m_statusItem.menu == nsMenu) + return; + + if (m_statusItem.menu) { + [NSNotificationCenter.defaultCenter removeObserver:m_delegate + name:NSMenuDidBeginTrackingNotification + object:m_statusItem.menu + ]; + } + + m_statusItem.menu = nsMenu; if (m_statusItem.menu) { // When a menu is assigned, NSStatusBarButtonCell will intercept the mouse diff --git a/src/plugins/platforms/cocoa/qcocoatheme.h b/src/plugins/platforms/cocoa/qcocoatheme.h index a7c37a685c..97e0f633a7 100644 --- a/src/plugins/platforms/cocoa/qcocoatheme.h +++ b/src/plugins/platforms/cocoa/qcocoatheme.h @@ -35,6 +35,7 @@ public: const QFont *font(Font type = SystemFont) const override; QPixmap standardPixmap(StandardPixmap sp, const QSizeF &size) const override; QIcon fileIcon(const QFileInfo &fileInfo, QPlatformTheme::IconOptions options = {}) const override; + QIconEngine *createIconEngine(const QString &iconName) const override; QVariant themeHint(ThemeHint hint) const override; Qt::ColorScheme colorScheme() const override; @@ -43,6 +44,7 @@ public: static const char *name; + void requestColorScheme(Qt::ColorScheme scheme) override; void handleSystemThemeChange(); #ifndef QT_NO_SHORTCUT @@ -54,6 +56,9 @@ private: QMacNotificationObserver m_systemColorObserver; mutable QHash<QPlatformTheme::Palette, QPalette*> m_palettes; QMacKeyValueObserver m_appearanceObserver; + + Qt::ColorScheme m_colorScheme = Qt::ColorScheme::Unknown; + void updateColorScheme(); }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/cocoa/qcocoatheme.mm b/src/plugins/platforms/cocoa/qcocoatheme.mm index 594df204e5..f3f3e05b50 100644 --- a/src/plugins/platforms/cocoa/qcocoatheme.mm +++ b/src/plugins/platforms/cocoa/qcocoatheme.mm @@ -5,7 +5,6 @@ #include "qcocoatheme.h" -#include <QtCore/QOperatingSystemVersion> #include <QtCore/QVariant> #include "qcocoasystemtrayicon.h" @@ -22,6 +21,7 @@ #include <QtGui/qpainter.h> #include <QtGui/qtextformat.h> #include <QtGui/private/qcoretextfontdatabase_p.h> +#include <QtGui/private/qappleiconengine_p.h> #include <QtGui/private/qfontengine_coretext_p.h> #include <QtGui/private/qabstractfileiconengine_p.h> #include <qpa/qplatformdialoghelper.h> @@ -94,6 +94,9 @@ static QPalette *qt_mac_createSystemPalette() palette->setColor(QPalette::Inactive, QPalette::PlaceholderText, qc); palette->setColor(QPalette::Disabled, QPalette::PlaceholderText, qc); + qc = qt_mac_toQColor([NSColor controlAccentColor]); + palette->setColor(QPalette::Accent, qc); + return palette; } @@ -210,17 +213,17 @@ const char *QCocoaTheme::name = "cocoa"; QCocoaTheme::QCocoaTheme() : m_systemPalette(nullptr) { - if (QOperatingSystemVersion::current() >= QOperatingSystemVersion::MacOSMojave) { - m_appearanceObserver = QMacKeyValueObserver(NSApp, @"effectiveAppearance", [this] { - NSAppearance.currentAppearance = NSApp.effectiveAppearance; - handleSystemThemeChange(); - }); - } + m_appearanceObserver = QMacKeyValueObserver(NSApp, @"effectiveAppearance", [this] { + NSAppearance.currentAppearance = NSApp.effectiveAppearance; + handleSystemThemeChange(); + }); m_systemColorObserver = QMacNotificationObserver(nil, NSSystemColorsDidChangeNotification, [this] { handleSystemThemeChange(); }); + + updateColorScheme(); } QCocoaTheme::~QCocoaTheme() @@ -239,6 +242,9 @@ void QCocoaTheme::reset() void QCocoaTheme::handleSystemThemeChange() { reset(); + + updateColorScheme(); + m_systemPalette = qt_mac_createSystemPalette(); m_palettes = qt_mac_createRolePalettes(); @@ -401,19 +407,8 @@ public: QPlatformTheme::IconOptions opts) : QAbstractFileIconEngine(info, opts) {} - static QList<QSize> availableIconSizes() - { - const qreal devicePixelRatio = qGuiApp->devicePixelRatio(); - const int sizes[] = { - qRound(16 * devicePixelRatio), qRound(32 * devicePixelRatio), - qRound(64 * devicePixelRatio), qRound(128 * devicePixelRatio), - qRound(256 * devicePixelRatio) - }; - return QAbstractFileIconEngine::toSizeList(sizes, sizes + sizeof(sizes) / sizeof(sizes[0])); - } - QList<QSize> availableSizes(QIcon::Mode = QIcon::Normal, QIcon::State = QIcon::Off) override - { return QCocoaFileIconEngine::availableIconSizes(); } + { return QAppleIconEngine::availableIconSizes(); } protected: QPixmap filePixmap(const QSize &size, QIcon::Mode, QIcon::State) override @@ -432,6 +427,11 @@ QIcon QCocoaTheme::fileIcon(const QFileInfo &fileInfo, QPlatformTheme::IconOptio return QIcon(new QCocoaFileIconEngine(fileInfo, iconOptions)); } +QIconEngine *QCocoaTheme::createIconEngine(const QString &iconName) const +{ + return new QAppleIconEngine(iconName); +} + QVariant QCocoaTheme::themeHint(ThemeHint hint) const { switch (hint) { @@ -445,7 +445,7 @@ QVariant QCocoaTheme::themeHint(ThemeHint hint) const return QVariant([[NSApplication sharedApplication] isFullKeyboardAccessEnabled] ? int(Qt::TabFocusAllControls) : int(Qt::TabFocusTextControls | Qt::TabFocusListControls)); case IconPixmapSizes: - return QVariant::fromValue(QCocoaFileIconEngine::availableIconSizes()); + return QVariant::fromValue(QAppleIconEngine::availableIconSizes()); case QPlatformTheme::PasswordMaskCharacter: return QVariant(QChar(0x2022)); case QPlatformTheme::UiEffects: @@ -472,7 +472,36 @@ QVariant QCocoaTheme::themeHint(ThemeHint hint) const Qt::ColorScheme QCocoaTheme::colorScheme() const { - return qt_mac_applicationIsInDarkMode() ? Qt::ColorScheme::Dark : Qt::ColorScheme::Light; + return m_colorScheme; +} + +void QCocoaTheme::requestColorScheme(Qt::ColorScheme scheme) +{ + NSAppearance *appearance = nil; + switch (scheme) { + case Qt::ColorScheme::Dark: + appearance = [NSAppearance appearanceNamed:NSAppearanceNameDarkAqua]; + break; + case Qt::ColorScheme::Light: + appearance = [NSAppearance appearanceNamed:NSAppearanceNameAqua]; + break; + case Qt::ColorScheme::Unknown: + break; + } + if (appearance != NSApp.effectiveAppearance) + NSApplication.sharedApplication.appearance = appearance; +} + +/* + Update the theme's color scheme based on the current appearance. + + We can only reference the appearance on the main thread, but the + CoreText font engine needs to know the color scheme, and might be + used from secondary threads, so we cache the color scheme. +*/ +void QCocoaTheme::updateColorScheme() +{ + m_colorScheme = qt_mac_applicationIsInDarkMode() ? Qt::ColorScheme::Dark : Qt::ColorScheme::Light; } QString QCocoaTheme::standardButtonText(int button) const @@ -490,12 +519,16 @@ QKeySequence QCocoaTheme::standardButtonShortcut(int button) const QPlatformMenuItem *QCocoaTheme::createPlatformMenuItem() const { - return new QCocoaMenuItem(); + auto *menuItem = new QCocoaMenuItem(); + qCDebug(lcQpaMenus) << "Created" << menuItem; + return menuItem; } QPlatformMenu *QCocoaTheme::createPlatformMenu() const { - return new QCocoaMenu(); + auto *menu = new QCocoaMenu(); + qCDebug(lcQpaMenus) << "Created" << menu; + return menu; } QPlatformMenuBar *QCocoaTheme::createPlatformMenuBar() const @@ -508,7 +541,9 @@ QPlatformMenuBar *QCocoaTheme::createPlatformMenuBar() const SLOT(onAppFocusWindowChanged(QWindow*))); } - return new QCocoaMenuBar(); + auto *menuBar = new QCocoaMenuBar(); + qCDebug(lcQpaMenus) << "Created" << menuBar; + return menuBar; } #ifndef QT_NO_SHORTCUT diff --git a/src/plugins/platforms/cocoa/qcocoawindow.h b/src/plugins/platforms/cocoa/qcocoawindow.h index ba1bc052fb..2036d4bf4c 100644 --- a/src/plugins/platforms/cocoa/qcocoawindow.h +++ b/src/plugins/platforms/cocoa/qcocoawindow.h @@ -80,6 +80,8 @@ public: QRect normalGeometry() const override; void setCocoaGeometry(const QRect &rect); + QMargins safeAreaMargins() const override; + void setVisible(bool visible) override; void setWindowFlags(Qt::WindowFlags flags) override; void setWindowState(Qt::WindowStates state) override; @@ -122,6 +124,7 @@ public: Q_NOTIFICATION_HANDLER(NSWindowDidMoveNotification) void windowDidMove(); Q_NOTIFICATION_HANDLER(NSWindowDidResizeNotification) void windowDidResize(); + Q_NOTIFICATION_HANDLER(NSWindowWillStartLiveResizeNotification) void windowWillStartLiveResize(); Q_NOTIFICATION_HANDLER(NSWindowDidEndLiveResizeNotification) void windowDidEndLiveResize(); Q_NOTIFICATION_HANDLER(NSWindowDidBecomeKeyNotification) void windowDidBecomeKey(); Q_NOTIFICATION_HANDLER(NSWindowDidResignKeyNotification) void windowDidResignKey(); @@ -186,6 +189,8 @@ public: Q_DECLARE_FLAGS(RecreationReasons, RecreationReason) Q_FLAG(RecreationReasons) + bool inLiveResize() const override; + protected: void recreateWindowIfNeeded(); QCocoaNSWindow *createNSWindow(bool shouldBePanel); @@ -232,6 +237,7 @@ public: // for QNSView bool m_inSetVisible = false; bool m_inSetGeometry = false; bool m_inSetStyleMask = false; + bool m_inLiveResize = false; QCocoaMenuBar *m_menubar = nullptr; @@ -241,6 +247,8 @@ public: // for QNSView int m_registerTouchCount = 0; bool m_resizableTransientParent = false; + QMargins m_lastReportedSafeAreaMargins; + static const int NoAlertRequest; NSInteger m_alertRequest = NoAlertRequest; diff --git a/src/plugins/platforms/cocoa/qcocoawindow.mm b/src/plugins/platforms/cocoa/qcocoawindow.mm index 205dfbaf9f..b4c8ae4ba1 100644 --- a/src/plugins/platforms/cocoa/qcocoawindow.mm +++ b/src/plugins/platforms/cocoa/qcocoawindow.mm @@ -24,6 +24,7 @@ #include <qpa/qplatformscreen.h> #include <QtGui/private/qcoregraphics_p.h> #include <QtGui/private/qhighdpiscaling_p.h> +#include <QtGui/private/qmetallayer_p.h> #include <QDebug> @@ -150,7 +151,10 @@ QCocoaWindow::~QCocoaWindow() QMacAutoReleasePool pool; [m_nsWindow makeFirstResponder:nil]; [m_nsWindow setContentView:nil]; - if ([m_view superview]) + + // Remove from superview only if we have a Qt window parent, + // as we don't want to affect window container foreign windows. + if (QPlatformWindow::parent()) [m_view removeFromSuperview]; // Make sure to disconnect observer in all case if view is valid @@ -174,8 +178,7 @@ QCocoaWindow::~QCocoaWindow() object:m_view]; [m_view release]; - [m_nsWindow close]; - [m_nsWindow release]; + [m_nsWindow closeAndRelease]; } QSurfaceFormat QCocoaWindow::format() const @@ -283,6 +286,54 @@ void QCocoaWindow::setCocoaGeometry(const QRect &rect) // will call QPlatformWindow::setGeometry(rect) during resize confirmation (see qnsview.mm) } +QMargins QCocoaWindow::safeAreaMargins() const +{ + // The safe area of the view reflects the area not covered by navigation + // bars, tab bars, toolbars, and other ancestor views that might obscure + // the current view (by setting additionalSafeAreaInsets). If the window + // uses NSWindowStyleMaskFullSizeContentView this also includes the area + // of the view covered by the title bar. + QMarginsF viewSafeAreaMargins = { + m_view.safeAreaInsets.left, + m_view.safeAreaInsets.top, + m_view.safeAreaInsets.right, + m_view.safeAreaInsets.bottom + }; + + // The screen's safe area insets represent the distances from the screen's + // edges at which content isn't obscured. The view's safe area margins do + // not include the screen's insets automatically, so we need to manually + // merge them. + auto screenRect = m_view.window.screen.frame; + auto screenInsets = m_view.window.screen.safeAreaInsets; + auto screenRelativeViewBounds = QCocoaScreen::mapFromNative( + [m_view.window convertRectToScreen: + [m_view convertRect:m_view.bounds toView:nil]] + ); + + // The margins are relative to the screen the window is on. + // Note that we do not want represent the area outside of the + // screen as being outside of the safe area. + QMarginsF screenSafeAreaMargins = { + screenInsets.left ? + qMax(0.0f, screenInsets.left - screenRelativeViewBounds.left()) + : 0.0f, + screenInsets.top ? + qMax(0.0f, screenInsets.top - screenRelativeViewBounds.top()) + : 0.0f, + screenInsets.right ? + qMax(0.0f, screenInsets.right + - (screenRect.size.width - screenRelativeViewBounds.right())) + : 0.0f, + screenInsets.bottom ? + qMax(0.0f, screenInsets.bottom + - (screenRect.size.height - screenRelativeViewBounds.bottom())) + : 0.0f + }; + + return (screenSafeAreaMargins | viewSafeAreaMargins).toMargins(); +} + bool QCocoaWindow::startSystemMove() { switch (NSApp.currentEvent.type) { @@ -306,6 +357,31 @@ void QCocoaWindow::setVisible(bool visible) { qCDebug(lcQpaWindow) << "QCocoaWindow::setVisible" << window() << visible; + // Our implementation of setVisible below is not idempotent, as for + // modal windows it calls beginSheet/endSheet or starts/ends modal + // sessions. However we can't simply guard for m_view.hidden already + // having the right state, as the behavior of this function differs + // based on whether the window has been initialized or not, as + // handleGeometryChange will bail out if the window is still + // initializing. Since we know we'll get a second setVisible + // call after creation, we can check for that case specifically, + // which means we can then safely guard on m_view.hidden changing. + + if (!m_initialized) { + qCDebug(lcQpaWindow) << "Window still initializing, skipping setting visibility"; + return; // We'll get another setVisible call after create is done + } + + if (visible == !m_view.hidden && (!isContentView() || visible == m_view.window.visible)) { + qCDebug(lcQpaWindow) << "No change in visible status. Ignoring."; + return; + } + + if (m_inSetVisible) { + qCWarning(lcQpaWindow) << "Already setting window visible!"; + return; + } + QScopedValueRollback<bool> rollback(m_inSetVisible, true); QMacAutoReleasePool pool; @@ -414,6 +490,24 @@ void QCocoaWindow::setVisible(bool visible) } } + // AppKit will in some cases set up the key view loop for child views, even if we + // don't set autorecalculatesKeyViewLoop, nor call recalculateKeyViewLoop ourselves. + // When a child window is promoted to a top level, AppKit will maintain the key view + // loop between the views, even if these views now cross NSWindows, even after we + // explicitly call recalculateKeyViewLoop. When the top level is then hidden, AppKit + // will complain when -[NSView _setHidden:setNeedsDisplay:] tries to transfer first + // responder by reading the nextValidKeyView, and it turns out to live in a different + // window. We mitigate this by a last second reset of the first responder, which is + // what AppKit also falls back to. It's unclear if the original situation of views + // having their nextKeyView pointing to views in other windows is kosher or not. + if (m_view.window.firstResponder == m_view && m_view.nextValidKeyView + && m_view.nextValidKeyView.window != m_view.window) { + qCDebug(lcQpaWindow) << "Detected nextValidKeyView" << m_view.nextValidKeyView + << "in different window" << m_view.nextValidKeyView.window + << "Resetting" << m_view.window << "first responder to nil."; + [m_view.window makeFirstResponder:nil]; + } + m_view.hidden = YES; if (parentCocoaWindow && window()->type() == Qt::Popup) { @@ -469,7 +563,7 @@ NSInteger QCocoaWindow::windowLevel(Qt::WindowFlags flags) auto *nsWindow = transientCocoaWindow->nativeWindow(); // We only upgrade the window level for "special" windows, to work - // around Qt Designer parenting the designer windows to the widget + // around Qt Widgets Designer parenting the designer windows to the widget // palette window (QTBUG-31779). This should be fixed in designer. if (type != Qt::Window) windowLevel = qMax(windowLevel, nsWindow.level); @@ -553,10 +647,17 @@ void QCocoaWindow::updateTitleBarButtons(Qt::WindowFlags windowFlags) bool hideButtons = true; for (const auto &[button, buttonHint] : buttons) { + // Set up Qt defaults based on window type bool enabled = true; + if (button == NSWindowMiniaturizeButton) + enabled = window()->type() != Qt::Dialog; + + // Let users override via CustomizeWindowHint if (windowFlags & Qt::CustomizeWindowHint) enabled = windowFlags & buttonHint; + // Then do some final sanitizations + if (button == NSWindowZoomButton && isFixedSize()) enabled = false; @@ -1184,8 +1285,26 @@ void QCocoaWindow::windowDidResize() handleWindowStateChanged(); } +void QCocoaWindow::windowWillStartLiveResize() +{ + // Track live resizing for all windows, including + // child windows, so we know if it's safe to update + // the window unthrottled outside of the main thread. + m_inLiveResize = true; +} + +bool QCocoaWindow::inLiveResize() const +{ + // Use member variable to track this instead of reflecting + // NSView.inLiveResize directly, so it can be called from + // non-main threads. + return m_inLiveResize; +} + void QCocoaWindow::windowDidEndLiveResize() { + m_inLiveResize = false; + if (!isContentView()) return; @@ -1194,32 +1313,33 @@ void QCocoaWindow::windowDidEndLiveResize() void QCocoaWindow::windowDidBecomeKey() { - if (!isContentView()) - return; - - if (isForeignWindow()) + // The NSWindow we're part of become key. Check if we're the first + // responder, and if so, deliver focus window change to our window. + if (m_view.window.firstResponder != m_view) return; - QNSView *firstResponderView = qt_objc_cast<QNSView *>(m_view.window.firstResponder); - if (!firstResponderView) - return; + qCDebug(lcQpaWindow) << m_view.window << "became key window." + << "Updating focus window to" << this << "with view" << m_view; - const QCocoaWindow *focusCocoaWindow = firstResponderView.platformWindow; - if (focusCocoaWindow->windowIsPopupType()) + if (windowIsPopupType()) { + qCDebug(lcQpaWindow) << "Window is popup. Skipping focus window change."; return; + } // See also [QNSView becomeFirstResponder] - QWindowSystemInterface::handleWindowActivated<QWindowSystemInterface::SynchronousDelivery>( - focusCocoaWindow->window(), Qt::ActiveWindowFocusReason); + QWindowSystemInterface::handleFocusWindowChanged<QWindowSystemInterface::SynchronousDelivery>( + window(), Qt::ActiveWindowFocusReason); } void QCocoaWindow::windowDidResignKey() { - if (!isContentView()) + // The NSWindow we're part of lost key. Check if we're the first + // responder, and if so, deliver window deactivation to our window. + if (m_view.window.firstResponder != m_view) return; - if (isForeignWindow()) - return; + qCDebug(lcQpaWindow) << m_view.window << "resigned key window." + << "Clearing focus window" << this << "with view" << m_view; // Make sure popups are closed before we deliver activation changes, which are // otherwise ignored by QApplication. @@ -1231,12 +1351,14 @@ void QCocoaWindow::windowDidResignKey() NSWindow *newKeyWindow = [NSApp keyWindow]; if (newKeyWindow && newKeyWindow != m_view.window && [newKeyWindow conformsToProtocol:@protocol(QNSWindowProtocol)]) { + qCDebug(lcQpaWindow) << "New key window" << newKeyWindow + << "is Qt window. Deferring focus window change."; return; } // Lost key window, go ahead and set the active window to zero if (!windowIsPopupType()) { - QWindowSystemInterface::handleWindowActivated<QWindowSystemInterface::SynchronousDelivery>( + QWindowSystemInterface::handleFocusWindowChanged<QWindowSystemInterface::SynchronousDelivery>( nullptr, Qt::ActiveWindowFocusReason); } } @@ -1273,8 +1395,14 @@ void QCocoaWindow::windowDidOrderOffScreen() void QCocoaWindow::windowDidChangeOcclusionState() { + // Note, we don't take the view's hiddenOrHasHiddenAncestor state into + // account here, but instead leave that up to handleExposeEvent, just + // like all the other signals that could potentially change the exposed + // state of the window. bool visible = m_view.window.occlusionState & NSWindowOcclusionStateVisible; - qCDebug(lcQpaWindow) << "QCocoaWindow::windowDidChangeOcclusionState" << window() << "is now" << (visible ? "visible" : "occluded"); + qCDebug(lcQpaWindow) << "Occlusion state of" << m_view.window << "for" + << window() << "changed to" << (visible ? "visible" : "occluded"); + if (visible) [m_view setNeedsDisplay:YES]; else @@ -1374,6 +1502,12 @@ void QCocoaWindow::handleGeometryChange() QWindowSystemInterface::handleGeometryChange(window(), newGeometry); + // Changing the window geometry may affect the safe area margins + if (safeAreaMargins() != m_lastReportedSafeAreaMargins) { + m_lastReportedSafeAreaMargins = safeAreaMargins(); + QWindowSystemInterface::handleSafeAreaMarginsChanged(window()); + } + // Guard against processing window system events during QWindow::setGeometry // calls, which Qt and Qt applications do not expect. if (!m_inSetGeometry) @@ -1441,23 +1575,31 @@ void QCocoaWindow::recreateWindowIfNeeded() { QMacAutoReleasePool pool; + QPlatformWindow *parentWindow = QPlatformWindow::parent(); + auto *parentCocoaWindow = static_cast<QCocoaWindow *>(parentWindow); + + QCocoaWindow *oldParentCocoaWindow = nullptr; + if (QNSView *qnsView = qnsview_cast(m_view.superview)) + oldParentCocoaWindow = qnsView.platformWindow; + if (isForeignWindow()) { // A foreign window is created as such, and can never move between being // foreign and not, so we don't need to get rid of any existing NSWindows, // nor create new ones, as a foreign window is a single simple NSView. qCDebug(lcQpaWindow) << "Skipping NSWindow management for foreign window" << this; + + // We do however need to manage the parent relationship + if (parentCocoaWindow) + [parentCocoaWindow->m_view addSubview:m_view]; + else if (oldParentCocoaWindow) + [m_view removeFromSuperview]; + return; } - QPlatformWindow *parentWindow = QPlatformWindow::parent(); - const bool isEmbeddedView = isEmbedded(); RecreationReasons recreateReason = RecreationNotNeeded; - QCocoaWindow *oldParentCocoaWindow = nullptr; - if (QNSView *qnsView = qnsview_cast(m_view.superview)) - oldParentCocoaWindow = qnsView.platformWindow; - if (parentWindow != oldParentCocoaWindow) recreateReason |= ParentChanged; @@ -1488,8 +1630,6 @@ void QCocoaWindow::recreateWindowIfNeeded() if (recreateReason == RecreationNotNeeded) return; - QCocoaWindow *parentCocoaWindow = static_cast<QCocoaWindow *>(parentWindow); - // Remove current window (if any) if ((isContentView() && !shouldBeContentView) || (recreateReason & PanelChanged)) { if (m_nsWindow) { @@ -1550,6 +1690,23 @@ bool QCocoaWindow::updatesWithDisplayLink() const void QCocoaWindow::deliverUpdateRequest() { qCDebug(lcQpaDrawing) << "Delivering update request to" << window(); + + if (auto *qtMetalLayer = qt_objc_cast<QMetalLayer*>(m_view.layer)) { + // We attempt a read lock here, so that the animation/render thread is + // prioritized lower than the main thread's displayLayer processing. + // Without this the two threads might fight over the next drawable, + // starving the main thread's presentation of the resized layer. + if (!qtMetalLayer.displayLock.tryLockForRead()) { + qCDebug(lcQpaDrawing) << "Deferring update request" + << "due to" << qtMetalLayer << "needing display"; + return; + } + + // But we don't hold the lock, as the update request can recurse + // back into setNeedsDisplay, which would deadlock. + qtMetalLayer.displayLock.unlock(); + } + QPlatformWindow::deliverUpdateRequest(); } @@ -1596,7 +1753,7 @@ void QCocoaWindow::setupPopupMonitor() | NSEventMaskMouseMoved; s_globalMouseMonitor = [NSEvent addGlobalMonitorForEventsMatchingMask:mouseButtonMask handler:^(NSEvent *e){ - if (!QGuiApplicationPrivate::instance()->popupActive()) { + if (!QGuiApplicationPrivate::instance()->activePopupWindow()) { removePopupMonitor(); return; } @@ -1728,8 +1885,9 @@ QCocoaNSWindow *QCocoaWindow::createNSWindow(bool shouldBePanel) // Qt::Tool windows hide on app deactivation, unless Qt::WA_MacAlwaysShowToolWindow is set nsWindow.hidesOnDeactivate = ((type & Qt::Tool) == Qt::Tool) && !alwaysShowToolWindow(); - // Make popup windows show on the same desktop as the parent full-screen window - nsWindow.collectionBehavior = NSWindowCollectionBehaviorFullScreenAuxiliary; + // Make popup windows show on the same desktop as the parent window + nsWindow.collectionBehavior = NSWindowCollectionBehaviorFullScreenAuxiliary + | NSWindowCollectionBehaviorMoveToActiveSpace; if ((type & Qt::Popup) == Qt::Popup) { nsWindow.hasShadow = YES; @@ -1744,11 +1902,15 @@ QCocoaNSWindow *QCocoaWindow::createNSWindow(bool shouldBePanel) applyContentBorderThickness(nsWindow); - if (QColorSpace colorSpace = format().colorSpace(); colorSpace.isValid()) { - NSData *iccData = colorSpace.iccProfile().toNSData(); - nsWindow.colorSpace = [[[NSColorSpace alloc] initWithICCProfileData:iccData] autorelease]; - qCDebug(lcQpaDrawing) << "Set" << this << "color space to" << nsWindow.colorSpace; - } + // We propagate the view's color space granulary to both the IOSurfaces + // used for QSurface::RasterSurface, as well as the CAMetalLayer used for + // QSurface::MetalSurface, but for QSurface::OpenGLSurface we don't have + // that option as we use NSOpenGLContext instead of CAOpenGLLayer. As a + // workaround we set the NSWindow's color space, which affects GL drawing + // with NSOpenGLContext as well. This does not conflict with the granular + // modifications we do to each surface for raster or Metal. + if (auto *qtView = qnsview_cast(m_view)) + nsWindow.colorSpace = qtView.colorSpace; return nsWindow; } @@ -1917,7 +2079,7 @@ qreal QCocoaWindow::devicePixelRatio() const { // The documented way to observe the relationship between device-independent // and device pixels is to use one for the convertToBacking functions. Other - // methods such as [NSWindow backingScaleFacor] might not give the correct + // methods such as [NSWindow backingScaleFactor] might not give the correct // result, for example if setWantsBestResolutionOpenGLSurface is not set or // or ignored by the OpenGL driver. NSSize backingSize = [m_view convertSizeToBacking:NSMakeSize(1.0, 1.0)]; @@ -1944,8 +2106,21 @@ bool QCocoaWindow::shouldRefuseKeyWindowAndFirstResponder() if (window()->flags() & (Qt::WindowDoesNotAcceptFocus | Qt::WindowTransparentForInput)) return true; - if (QWindowPrivate::get(window())->blockedByModalWindow) - return true; + // For application modal windows, as well as direct parent windows + // of window modal windows, AppKit takes care of blocking interaction. + // The Qt expectation however, is that all transient parents of a + // window modal window is blocked, as reflected by QGuiApplication. + // We reflect this by returning false from this function for transient + // parents blocked by a modal window, but limit it to the cases not + // covered by AppKit to avoid potential unwanted side effects. + QWindow *modalWindow = nullptr; + if (QGuiApplicationPrivate::instance()->isWindowBlocked(window(), &modalWindow)) { + if (modalWindow->modality() == Qt::WindowModal && modalWindow->transientParent() != window()) { + qCDebug(lcQpaWindow) << "Refusing key window for" << this << "due to being" + << "blocked by" << modalWindow; + return true; + } + } if (m_inSetVisible) { QVariant showWithoutActivating = window()->property("_q_showWithoutActivating"); diff --git a/src/plugins/platforms/cocoa/qmacclipboard.h b/src/plugins/platforms/cocoa/qmacclipboard.h index 75c112a10b..95267565f2 100644 --- a/src/plugins/platforms/cocoa/qmacclipboard.h +++ b/src/plugins/platforms/cocoa/qmacclipboard.h @@ -7,6 +7,8 @@ #include <QtGui> #include <QtGui/qutimimeconverter.h> +#include <QtCore/qpointer.h> + #include <ApplicationServices/ApplicationServices.h> QT_BEGIN_NAMESPACE diff --git a/src/plugins/platforms/cocoa/qnsview.h b/src/plugins/platforms/cocoa/qnsview.h index e41f5a7296..7f845a5c3b 100644 --- a/src/plugins/platforms/cocoa/qnsview.h +++ b/src/plugins/platforms/cocoa/qnsview.h @@ -32,6 +32,12 @@ QT_DECLARE_NAMESPACED_OBJC_INTERFACE(QNSView, NSView - (void)cancelComposingText; @end +Q_FORWARD_DECLARE_OBJC_CLASS(NSColorSpace); + +@interface QNSView (DrawingAPI) +@property (nonatomic, readonly) NSColorSpace *colorSpace; +@end + @interface QNSView (QtExtras) @property (nonatomic, readonly) QCocoaWindow *platformWindow; @end diff --git a/src/plugins/platforms/cocoa/qnsview.mm b/src/plugins/platforms/cocoa/qnsview.mm index 1d59a0c03e..eb998b0409 100644 --- a/src/plugins/platforms/cocoa/qnsview.mm +++ b/src/plugins/platforms/cocoa/qnsview.mm @@ -5,6 +5,7 @@ #include <AppKit/AppKit.h> #include <MetalKit/MetalKit.h> +#include <UniformTypeIdentifiers/UTCoreTypes.h> #include "qnsview.h" #include "qcocoawindow.h" @@ -20,7 +21,6 @@ #include <QtCore/QDebug> #include <QtCore/QPointer> #include <QtCore/QSet> -#include <QtCore/qsysinfo.h> #include <QtCore/private/qcore_mac_p.h> #include <QtGui/QAccessible> #include <QtGui/QImage> @@ -35,6 +35,9 @@ #endif #include "qcocoaintegration.h" #include <QtGui/private/qmacmimeregistry_p.h> +#include <QtGui/private/qmetallayer_p.h> + +#include <QuartzCore/CATransaction.h> @interface QNSView (Drawing) <CALayerDelegate> - (void)initDrawing; @@ -90,6 +93,7 @@ QT_NAMESPACE_ALIAS_OBJC_CLASS(QNSViewMenuHelper); @property (assign) NSView* previousSuperview; @property (assign) NSWindow* previousWindow; @property (retain) QNSViewMenuHelper* menuHelper; +@property (nonatomic, retain) NSColorSpace *colorSpace; @end @implementation QNSView { @@ -119,6 +123,8 @@ QT_NAMESPACE_ALIAS_OBJC_CLASS(QNSViewMenuHelper); NSDraggingContext m_lastSeenContext; } +@synthesize colorSpace = m_colorSpace; + - (instancetype)initWithCocoaWindow:(QCocoaWindow *)platformWindow { if ((self = [super initWithFrame:NSZeroRect])) { @@ -273,15 +279,29 @@ QT_NAMESPACE_ALIAS_OBJC_CLASS(QNSViewMenuHelper); return focusWindow; } +/* + Invoked when the view is hidden, either directly, + or in response to an ancestor being hidden. +*/ - (void)viewDidHide { + qCDebug(lcQpaWindow) << "Did hide" << self; + if (!m_platformWindow->isExposed()) return; m_platformWindow->handleExposeEvent(QRegion()); +} + +/* + Invoked when the view is unhidden, either directly, + or in response to an ancestor being unhidden. +*/ +- (void)viewDidUnhide +{ + qCDebug(lcQpaWindow) << "Did unhide" << self; - // Note: setNeedsDisplay is automatically called for - // viewDidUnhide so no reason to override it here. + [self setNeedsDisplay:YES]; } - (BOOL)isTransparentForUserInput @@ -314,7 +334,7 @@ QT_NAMESPACE_ALIAS_OBJC_CLASS(QNSViewMenuHelper); // QWindow activation from QCocoaWindow::windowDidBecomeKey instead. The only // exception is if the window can never become key, in which case we naturally // cannot wait for that to happen. - QWindowSystemInterface::handleWindowActivated<QWindowSystemInterface::SynchronousDelivery>( + QWindowSystemInterface::handleFocusWindowChanged<QWindowSystemInterface::SynchronousDelivery>( [self topLevelWindow], Qt::ActiveWindowFocusReason); } diff --git a/src/plugins/platforms/cocoa/qnsview_accessibility.mm b/src/plugins/platforms/cocoa/qnsview_accessibility.mm index 3f3898fd18..e781f21a6c 100644 --- a/src/plugins/platforms/cocoa/qnsview_accessibility.mm +++ b/src/plugins/platforms/cocoa/qnsview_accessibility.mm @@ -13,6 +13,15 @@ @implementation QNSView (Accessibility) +- (void)activateQtAccessibility +{ + // Activate the Qt accessibility machinery for all entry points + // below that may be triggered by system accessibility queries, + // as otherwise Qt is not aware that the system needs to know + // about all accessibility state changes in Qt. + QCocoaIntegration::instance()->accessibility()->setActive(true); +} + - (id)childAccessibleElement { QCocoaWindow *platformWindow = self.platformWindow; @@ -32,8 +41,7 @@ - (id)accessibilityAttributeValue:(NSString *)attribute { - // activate accessibility updates - QCocoaIntegration::instance()->accessibility()->setActive(true); + [self activateQtAccessibility]; if ([attribute isEqualToString:NSAccessibilityChildrenAttribute]) return NSAccessibilityUnignoredChildrenForOnlyChild([self childAccessibleElement]); @@ -43,11 +51,13 @@ - (id)accessibilityHitTest:(NSPoint)point { + [self activateQtAccessibility]; return [[self childAccessibleElement] accessibilityHitTest:point]; } - (id)accessibilityFocusedUIElement { + [self activateQtAccessibility]; return [[self childAccessibleElement] accessibilityFocusedUIElement]; } diff --git a/src/plugins/platforms/cocoa/qnsview_complextext.mm b/src/plugins/platforms/cocoa/qnsview_complextext.mm index 3ccaf8269e..d7f8f4baf0 100644 --- a/src/plugins/platforms/cocoa/qnsview_complextext.mm +++ b/src/plugins/platforms/cocoa/qnsview_complextext.mm @@ -130,8 +130,8 @@ newlineEvent.key = isEnter ? Qt::Key_Enter : Qt::Key_Return; newlineEvent.text = isEnter ? QLatin1Char(kEnterCharCode) : QLatin1Char(kReturnCharCode); - newlineEvent.nativeVirtualKey = isEnter ? kVK_ANSI_KeypadEnter - : kVK_Return; + newlineEvent.nativeVirtualKey = isEnter ? quint32(kVK_ANSI_KeypadEnter) + : quint32(kVK_Return); qCDebug(lcQpaKeys) << "Inserting newline via" << newlineEvent; newlineEvent.sendWindowSystemEvent(m_platformWindow->window()); @@ -505,6 +505,32 @@ return NSNotFound; } +/* + Returns the window level of the text input. + + This allows the input method to place its input panel + above the text input. +*/ +- (NSInteger)windowLevel +{ + // The default level assumed by input methods is NSFloatingWindowLevel, + // but our NSWindow level could be higher than that for many reasons, + // including being set via QWindow::setFlags() or directly on the + // NSWindow, or because we're embedded into a native view hierarchy. + // Return the actual window level to account for this. + auto level = m_platformWindow ? m_platformWindow->nativeWindow().level + : NSNormalWindowLevel; + + // The logic above only covers our own window though. In some cases, + // such as when a completer is active, the text input has a lower + // window level than another window that's also visible, and we don't + // want the input panel to be sandwiched between these two windows. + // Account for this by explicitly using NSPopUpMenuWindowLevel as + // the minimum window level, which corresponds to the highest level + // one can get via QWindow::setFlags(), except for Qt::ToolTip. + return qMax(level, NSPopUpMenuWindowLevel); +} + // ------------- Helper functions ------------- /* diff --git a/src/plugins/platforms/cocoa/qnsview_dragging.mm b/src/plugins/platforms/cocoa/qnsview_dragging.mm index f5bb25c300..4f7d35a0d6 100644 --- a/src/plugins/platforms/cocoa/qnsview_dragging.mm +++ b/src/plugins/platforms/cocoa/qnsview_dragging.mm @@ -16,8 +16,8 @@ NSPasteboardTypeRTF, NSPasteboardTypeTabularText, NSPasteboardTypeFont, NSPasteboardTypeRuler, NSFileContentsPboardType, NSPasteboardTypeRTFD , NSPasteboardTypeHTML, - NSPasteboardTypeURL, NSPasteboardTypePDF, (NSString *)kUTTypeVCard, - (NSString *)kPasteboardTypeFileURLPromise, (NSString *)kUTTypeInkText, + NSPasteboardTypeURL, NSPasteboardTypePDF, UTTypeVCard.identifier, + (NSString *)kPasteboardTypeFileURLPromise, NSPasteboardTypeMultipleTextSelection, mimeTypeGeneric]]; // Add custom types supported by the application diff --git a/src/plugins/platforms/cocoa/qnsview_drawing.mm b/src/plugins/platforms/cocoa/qnsview_drawing.mm index 77fa8259ac..61691ab4fb 100644 --- a/src/plugins/platforms/cocoa/qnsview_drawing.mm +++ b/src/plugins/platforms/cocoa/qnsview_drawing.mm @@ -13,6 +13,14 @@ << " QT_MAC_WANTS_LAYER/_q_mac_wantsLayer has no effect."; } + // Pick up and persist requested color space from surface format + const QSurfaceFormat surfaceFormat = m_platformWindow->format(); + if (QColorSpace colorSpace = surfaceFormat.colorSpace(); colorSpace.isValid()) { + NSData *iccData = colorSpace.iccProfile().toNSData(); + self.colorSpace = [[[NSColorSpace alloc] initWithICCProfileData:iccData] autorelease]; + } + + // Trigger creation of the layer self.wantsLayer = YES; } @@ -28,6 +36,12 @@ return YES; } +- (NSColorSpace*)colorSpace +{ + // If no explicit color space was set, use the NSWindow's color space + return m_colorSpace ? m_colorSpace : self.window.colorSpace; +} + // ----------------------- Layer setup ----------------------- - (BOOL)shouldUseMetalLayer @@ -61,7 +75,10 @@ // too late at this point and the QWindow will be non-functional, // but we can at least print a warning. if ([MTLCreateSystemDefaultDevice() autorelease]) { - return [CAMetalLayer layer]; + static bool allowPresentsWithTransaction = + !qEnvironmentVariableIsSet("QT_MTL_NO_TRANSACTION"); + return allowPresentsWithTransaction ? + [QMetalLayer layer] : [CAMetalLayer layer]; } else { qCWarning(lcQpaDrawing) << "Failed to create QWindow::MetalSurface." << "Metal is not supported by any of the GPUs in this system."; @@ -93,12 +110,7 @@ [super setLayer:layer]; - // When adding a view to a view hierarchy the backing properties will change - // which results in updating the contents scale, but in case of switching the - // layer on a view that's already in a view hierarchy we need to manually ensure - // the scale is up to date. - if (self.superview) - [self updateLayerContentsScale]; + [self propagateBackingProperties]; if (self.opaque && lcQpaDrawing().isDebugEnabled()) { // If the view claims to be opaque we expect it to fill the entire @@ -131,8 +143,7 @@ { qCDebug(lcQpaDrawing) << "Backing properties changed for" << self; - if (self.layer) - [self updateLayerContentsScale]; + [self propagateBackingProperties]; // Ideally we would plumb this situation through QPA in a way that lets // clients invalidate their own caches, recreate QBackingStore, etc. @@ -141,8 +152,11 @@ [self setNeedsDisplay:YES]; } -- (void)updateLayerContentsScale +- (void)propagateBackingProperties { + if (!self.layer) + return; + // We expect clients to fill the layer with retina aware content, // based on the devicePixelRatio of the QWindow, so we set the // layer's content scale to match that. By going via devicePixelRatio @@ -153,6 +167,12 @@ auto devicePixelRatio = m_platformWindow->devicePixelRatio(); qCDebug(lcQpaDrawing) << "Updating" << self.layer << "content scale to" << devicePixelRatio; self.layer.contentsScale = devicePixelRatio; + + if ([self.layer isKindOfClass:CAMetalLayer.class]) { + CAMetalLayer *metalLayer = static_cast<CAMetalLayer *>(self.layer); + metalLayer.colorspace = self.colorSpace.CGColorSpace; + qCDebug(lcQpaDrawing) << "Set" << metalLayer << "color space to" << metalLayer.colorspace; + } } /* @@ -205,8 +225,39 @@ return; } - qCDebug(lcQpaDrawing) << "[QNSView displayLayer]" << m_platformWindow->window(); - m_platformWindow->handleExposeEvent(QRectF::fromCGRect(self.bounds).toRect()); + const auto handleExposeEvent = [&]{ + const auto bounds = QRectF::fromCGRect(self.bounds).toRect(); + qCDebug(lcQpaDrawing) << "[QNSView displayLayer]" << m_platformWindow->window() << bounds; + m_platformWindow->handleExposeEvent(bounds); + }; + + if (auto *qtMetalLayer = qt_objc_cast<QMetalLayer*>(self.layer)) { + const bool presentedWithTransaction = qtMetalLayer.presentsWithTransaction; + qtMetalLayer.presentsWithTransaction = YES; + + handleExposeEvent(); + + // If the expose event resulted in a secondary thread requesting that its + // drawable should be presented on the main thread with transaction, do so. + if (auto mainThreadPresentation = qtMetalLayer.mainThreadPresentation) { + mainThreadPresentation(); + qtMetalLayer.mainThreadPresentation = nil; + } + + qtMetalLayer.presentsWithTransaction = presentedWithTransaction; + + // We're done presenting, but we must wait to unlock the display lock + // until the display cycle finishes, as otherwise the render thread may + // step in and present before the transaction commits. The display lock + // is recursive, so setNeedsDisplay can be safely called in the meantime + // without any issue. + QMetaObject::invokeMethod(m_platformWindow, [qtMetalLayer]{ + qCDebug(lcMetalLayer) << "Unlocking" << qtMetalLayer << "after finishing display-cycle"; + qtMetalLayer.displayLock.unlock(); + }, Qt::QueuedConnection); + } else { + handleExposeEvent(); + } } @end diff --git a/src/plugins/platforms/cocoa/qnsview_gestures.mm b/src/plugins/platforms/cocoa/qnsview_gestures.mm index 7c64e3356f..9c5ead072b 100644 --- a/src/plugins/platforms/cocoa/qnsview_gestures.mm +++ b/src/plugins/platforms/cocoa/qnsview_gestures.mm @@ -11,9 +11,6 @@ Q_LOGGING_CATEGORY(lcQpaGestures, "qt.qpa.input.gestures") - (bool)handleGestureAsBeginEnd:(NSEvent *)event { - if (QOperatingSystemVersion::current() < QOperatingSystemVersion::OSXElCapitan) - return false; - if ([event phase] == NSEventPhaseBegan) { [self beginGestureWithEvent:event]; return true; diff --git a/src/plugins/platforms/cocoa/qnsview_keys.mm b/src/plugins/platforms/cocoa/qnsview_keys.mm index 118678ffa5..abee622e65 100644 --- a/src/plugins/platforms/cocoa/qnsview_keys.mm +++ b/src/plugins/platforms/cocoa/qnsview_keys.mm @@ -30,8 +30,36 @@ static bool isSpecialKey(const QString &text) return false; } +static bool sendAsShortcut(const KeyEvent &keyEvent, QWindow *window) +{ + KeyEvent shortcutEvent = keyEvent; + shortcutEvent.type = QEvent::Shortcut; + qCDebug(lcQpaKeys) << "Trying potential shortcuts in" << window + << "for" << shortcutEvent; + + if (shortcutEvent.sendWindowSystemEvent(window)) { + qCDebug(lcQpaKeys) << "Found matching shortcut; will not send as key event"; + return true; + } + qCDebug(lcQpaKeys) << "No matching shortcuts; continuing with key event delivery"; + return false; +} + @implementation QNSView (Keys) +- (bool)performKeyEquivalent:(NSEvent *)nsevent +{ + // Implemented to handle shortcuts for modified Tab keys, which are + // handled by Cocoa and not delivered to your keyDown implementation. + if (nsevent.type == NSEventTypeKeyDown && m_composingText.isEmpty()) { + const bool ctrlDown = [nsevent modifierFlags] & NSEventModifierFlagControl; + const bool isTabKey = nsevent.keyCode == kVK_Tab; + if (ctrlDown && isTabKey && sendAsShortcut(KeyEvent(nsevent), [self topLevelWindow])) + return YES; + } + return NO; +} + - (bool)handleKeyEvent:(NSEvent *)nsevent { qCDebug(lcQpaKeys) << "Handling" << nsevent; @@ -52,17 +80,8 @@ static bool isSpecialKey(const QString &text) if (keyEvent.type == QEvent::KeyPress) { if (m_composingText.isEmpty()) { - KeyEvent shortcutEvent = keyEvent; - shortcutEvent.type = QEvent::Shortcut; - qCDebug(lcQpaKeys) << "Trying potential shortcuts in" << window - << "for" << shortcutEvent; - - if (shortcutEvent.sendWindowSystemEvent(window)) { - qCDebug(lcQpaKeys) << "Found matching shortcut; will not send as key event"; + if (sendAsShortcut(keyEvent, window)) return true; - } else { - qCDebug(lcQpaKeys) << "No matching shortcuts; continuing with key event delivery"; - } } QObject *focusObject = m_platformWindow ? m_platformWindow->window()->focusObject() : nullptr; @@ -94,7 +113,10 @@ static bool isSpecialKey(const QString &text) qCDebug(lcQpaKeys) << "Interpreting key event for focus object" << focusObject; m_currentlyInterpretedKeyEvent = nsevent; - [self interpretKeyEvents:@[nsevent]]; + if (![self.inputContext handleEvent:nsevent]) { + qCDebug(lcQpaKeys) << "Input context did not consume event"; + m_sendKeyEvent = true; + } m_currentlyInterpretedKeyEvent = 0; didInterpretKeyEvent = true; diff --git a/src/plugins/platforms/cocoa/qnsview_mouse.mm b/src/plugins/platforms/cocoa/qnsview_mouse.mm index 396029767c..2fd57fe68e 100644 --- a/src/plugins/platforms/cocoa/qnsview_mouse.mm +++ b/src/plugins/platforms/cocoa/qnsview_mouse.mm @@ -543,6 +543,30 @@ static const QPointingDevice *pointingDeviceFor(qint64 deviceID) [self handleMouseEvent: theEvent]; } +- (BOOL)shouldPropagateMouseEnterExit +{ + Q_ASSERT(m_platformWindow); + + // We send out enter and leave events mainly from mouse move events (mouseMovedImpl), + // but in some case (see mouseEnteredImpl:) we also want to propagate enter/leave + // events from the platform. We only do this for windows that themselves are not + // handled by another parent QWindow. + + if (m_platformWindow->isContentView()) + return true; + + // Windows manually embedded into a native view does not have a QWindow parent + if (m_platformWindow->isEmbedded()) + return true; + + // Windows embedded via fromWinId do, but the parent isn't a QNSView + QPlatformWindow *parentWindow = m_platformWindow->QPlatformWindow::parent(); + if (parentWindow && parentWindow->isForeignWindow()) + return true; + + return false; +} + - (void)mouseEnteredImpl:(NSEvent *)theEvent { Q_UNUSED(theEvent); @@ -566,8 +590,7 @@ static const QPointingDevice *pointingDeviceFor(qint64 deviceID) // in time (s_windowUnderMouse). The latter is also used to also send out enter/leave // events when the application is activated/deactivated. - // Root (top level or embedded) windows generate enter events for sub-windows - if (!m_platformWindow->isContentView() && !m_platformWindow->isEmbedded()) + if (![self shouldPropagateMouseEnterExit]) return; QPointF windowPoint; @@ -593,8 +616,7 @@ static const QPointingDevice *pointingDeviceFor(qint64 deviceID) if (!m_platformWindow) return; - // Root (top level or embedded) windows generate enter events for sub-windows - if (!m_platformWindow->isContentView() && !m_platformWindow->isEmbedded()) + if (![self shouldPropagateMouseEnterExit]) return; QCocoaWindow *windowToLeave = QCocoaWindow::s_windowUnderMouse; diff --git a/src/plugins/platforms/cocoa/qnsview_touch.mm b/src/plugins/platforms/cocoa/qnsview_touch.mm index 6a147701fc..97ed5b7624 100644 --- a/src/plugins/platforms/cocoa/qnsview_touch.mm +++ b/src/plugins/platforms/cocoa/qnsview_touch.mm @@ -25,7 +25,10 @@ Q_LOGGING_CATEGORY(lcQpaTouch, "qt.qpa.input.touch") const NSTimeInterval timestamp = [event timestamp]; const QList<QWindowSystemInterface::TouchPoint> points = QCocoaTouch::getCurrentTouchPointList(event, [self shouldSendSingleTouch]); qCDebug(lcQpaTouch) << "touchesBeganWithEvent" << points << "from device" << Qt::hex << [event deviceID]; - QWindowSystemInterface::handleTouchEvent(m_platformWindow->window(), timestamp * 1000, QCocoaTouch::getTouchDevice(QInputDevice::DeviceType::TouchPad, [event deviceID]), points); + QWindowSystemInterface::handleTouchEvent<QWindowSystemInterface::SynchronousDelivery>( + m_platformWindow->window(), timestamp * 1000, + QCocoaTouch::getTouchDevice(QInputDevice::DeviceType::TouchPad, [event deviceID]), + points); } - (void)touchesMovedWithEvent:(NSEvent *)event @@ -36,7 +39,10 @@ Q_LOGGING_CATEGORY(lcQpaTouch, "qt.qpa.input.touch") const NSTimeInterval timestamp = [event timestamp]; const QList<QWindowSystemInterface::TouchPoint> points = QCocoaTouch::getCurrentTouchPointList(event, [self shouldSendSingleTouch]); qCDebug(lcQpaTouch) << "touchesMovedWithEvent" << points << "from device" << Qt::hex << [event deviceID]; - QWindowSystemInterface::handleTouchEvent(m_platformWindow->window(), timestamp * 1000, QCocoaTouch::getTouchDevice(QInputDevice::DeviceType::TouchPad, [event deviceID]), points); + QWindowSystemInterface::handleTouchEvent<QWindowSystemInterface::SynchronousDelivery>( + m_platformWindow->window(), timestamp * 1000, + QCocoaTouch::getTouchDevice(QInputDevice::DeviceType::TouchPad, [event deviceID]), + points); } - (void)touchesEndedWithEvent:(NSEvent *)event @@ -47,7 +53,10 @@ Q_LOGGING_CATEGORY(lcQpaTouch, "qt.qpa.input.touch") const NSTimeInterval timestamp = [event timestamp]; const QList<QWindowSystemInterface::TouchPoint> points = QCocoaTouch::getCurrentTouchPointList(event, [self shouldSendSingleTouch]); qCDebug(lcQpaTouch) << "touchesEndedWithEvent" << points << "from device" << Qt::hex << [event deviceID]; - QWindowSystemInterface::handleTouchEvent(m_platformWindow->window(), timestamp * 1000, QCocoaTouch::getTouchDevice(QInputDevice::DeviceType::TouchPad, [event deviceID]), points); + QWindowSystemInterface::handleTouchEvent<QWindowSystemInterface::SynchronousDelivery>( + m_platformWindow->window(), timestamp * 1000, + QCocoaTouch::getTouchDevice(QInputDevice::DeviceType::TouchPad, [event deviceID]), + points); } - (void)touchesCancelledWithEvent:(NSEvent *)event @@ -58,7 +67,10 @@ Q_LOGGING_CATEGORY(lcQpaTouch, "qt.qpa.input.touch") const NSTimeInterval timestamp = [event timestamp]; const QList<QWindowSystemInterface::TouchPoint> points = QCocoaTouch::getCurrentTouchPointList(event, [self shouldSendSingleTouch]); qCDebug(lcQpaTouch) << "touchesCancelledWithEvent" << points << "from device" << Qt::hex << [event deviceID]; - QWindowSystemInterface::handleTouchEvent(m_platformWindow->window(), timestamp * 1000, QCocoaTouch::getTouchDevice(QInputDevice::DeviceType::TouchPad, [event deviceID]), points); + QWindowSystemInterface::handleTouchEvent<QWindowSystemInterface::SynchronousDelivery>( + m_platformWindow->window(), timestamp * 1000, + QCocoaTouch::getTouchDevice(QInputDevice::DeviceType::TouchPad, [event deviceID]), + points); } @end diff --git a/src/plugins/platforms/cocoa/qnswindow.h b/src/plugins/platforms/cocoa/qnswindow.h index f69e809133..8f842eba85 100644 --- a/src/plugins/platforms/cocoa/qnswindow.h +++ b/src/plugins/platforms/cocoa/qnswindow.h @@ -36,6 +36,8 @@ QT_FORWARD_DECLARE_CLASS(QCocoaWindow) typedef NSWindow<QNSWindowProtocol> QCocoaNSWindow; +QCocoaNSWindow *qnswindow_cast(NSWindow *window); + #else class QCocoaNSWindow; #endif // __OBJC__ diff --git a/src/plugins/platforms/cocoa/qnswindow.mm b/src/plugins/platforms/cocoa/qnswindow.mm index 6244d5d129..f536045fec 100644 --- a/src/plugins/platforms/cocoa/qnswindow.mm +++ b/src/plugins/platforms/cocoa/qnswindow.mm @@ -12,7 +12,6 @@ #include "qcocoaintegration.h" #include <qpa/qwindowsysteminterface.h> -#include <qoperatingsystemversion.h> Q_LOGGING_CATEGORY(lcQpaEvents, "qt.qpa.events"); @@ -58,6 +57,15 @@ static bool isMouseEvent(NSEvent *ev) } @end + +NSWindow<QNSWindowProtocol> *qnswindow_cast(NSWindow *window) +{ + if ([window conformsToProtocol:@protocol(QNSWindowProtocol)]) + return static_cast<QCocoaNSWindow *>(window); + else + return nil; +} + @implementation QNSWindow #define QNSWINDOW_PROTOCOL_IMPLMENTATION 1 #include "qnswindow.mm" @@ -98,9 +106,10 @@ static bool isMouseEvent(NSEvent *ev) continue; if ([window conformsToProtocol:@protocol(QNSWindowProtocol)]) { - QCocoaWindow *cocoaWindow = static_cast<QCocoaNSWindow *>(window).platformWindow; - window.level = notification.name == NSApplicationWillResignActiveNotification ? - NSNormalWindowLevel : cocoaWindow->windowLevel(cocoaWindow->window()->flags()); + if (QCocoaWindow *cocoaWindow = static_cast<QCocoaNSWindow *>(window).platformWindow) { + window.level = notification.name == NSApplicationWillResignActiveNotification ? + NSNormalWindowLevel : cocoaWindow->windowLevel(cocoaWindow->window()->flags()); + } } // The documentation says that "when a window enters a new level, it’s ordered @@ -216,6 +225,7 @@ static bool isMouseEvent(NSEvent *ev) m_platformWindow->setWindowFilePath(window->filePath()); // Also sets window icon m_platformWindow->setWindowState(window->windowState()); m_platformWindow->setOpacity(window->opacity()); + m_platformWindow->setVisible(window->isVisible()); } - (NSString *)description @@ -340,7 +350,7 @@ static bool isMouseEvent(NSEvent *ev) // not Qt). However, an active popup is expected to grab any mouse event within the // application, so we need to handle those explicitly and trust Qt's isWindowBlocked // implementation to eat events that shouldn't be delivered anyway. - if (isMouseEvent(theEvent) && QGuiApplicationPrivate::instance()->popupActive() + if (isMouseEvent(theEvent) && QGuiApplicationPrivate::instance()->activePopupWindow() && QGuiApplicationPrivate::instance()->isWindowBlocked(m_platformWindow->window(), nullptr)) { qCDebug(lcQpaWindow) << "Mouse event over modally blocked window" << m_platformWindow->window() << "while popup is open - redirecting"; diff --git a/src/plugins/platforms/cocoa/qnswindowdelegate.mm b/src/plugins/platforms/cocoa/qnswindowdelegate.mm index 2d90fbf544..1db7772771 100644 --- a/src/plugins/platforms/cocoa/qnswindowdelegate.mm +++ b/src/plugins/platforms/cocoa/qnswindowdelegate.mm @@ -23,9 +23,7 @@ static inline bool isWhiteSpace(const QString &s) static QCocoaWindow *toPlatformWindow(NSWindow *window) { - if ([window conformsToProtocol:@protocol(QNSWindowProtocol)]) - return static_cast<QCocoaNSWindow *>(window).platformWindow; - return nullptr; + return qnswindow_cast(window).platformWindow; } @implementation QNSWindowDelegate |