diff options
Diffstat (limited to 'src/plugins/platforms/cocoa/qcocoascreen.mm')
-rw-r--r-- | src/plugins/platforms/cocoa/qcocoascreen.mm | 553 |
1 files changed, 368 insertions, 185 deletions
diff --git a/src/plugins/platforms/cocoa/qcocoascreen.mm b/src/plugins/platforms/cocoa/qcocoascreen.mm index bd5c95b9d0..be562e5455 100644 --- a/src/plugins/platforms/cocoa/qcocoascreen.mm +++ b/src/plugins/platforms/cocoa/qcocoascreen.mm @@ -1,41 +1,7 @@ -/**************************************************************************** -** -** Copyright (C) 2017 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the plugins of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2017 The Qt Company Ltd. +// 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 "qcocoascreen.h" @@ -49,7 +15,9 @@ #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> QT_BEGIN_NAMESPACE @@ -57,6 +25,7 @@ QT_BEGIN_NAMESPACE namespace CoreGraphics { Q_NAMESPACE enum DisplayChange { + ReconfiguredWithFlagsMissing = 0, Moved = kCGDisplayMovedFlag, SetMain = kCGDisplaySetMainFlag, SetMode = kCGDisplaySetModeFlag, @@ -71,73 +40,119 @@ namespace CoreGraphics { Q_ENUM_NS(DisplayChange) } +QMacNotificationObserver QCocoaScreen::s_screenParameterObserver; +CGDisplayReconfigurationCallBack QCocoaScreen::s_displayReconfigurationCallBack = nullptr; + void QCocoaScreen::initializeScreens() { - uint32_t displayCount = 0; - if (CGGetActiveDisplayList(0, nullptr, &displayCount) != kCGErrorSuccess) - qFatal("Failed to get number of active displays"); + updateScreens(); - CGDirectDisplayID activeDisplays[displayCount]; - if (CGGetActiveDisplayList(displayCount, &activeDisplays[0], &displayCount) != kCGErrorSuccess) - qFatal("Failed to get active displays"); + s_displayReconfigurationCallBack = [](CGDirectDisplayID displayId, CGDisplayChangeSummaryFlags flags, void *userInfo) { + Q_UNUSED(userInfo); - for (CGDirectDisplayID displayId : activeDisplays) - QCocoaScreen::add(displayId); + const bool beforeReconfigure = flags & kCGDisplayBeginConfigurationFlag; + qCDebug(lcQpaScreen).verbosity(0) << "Display" << displayId + << (beforeReconfigure ? "beginning" : "finished") << "reconfigure" + << QFlags<CoreGraphics::DisplayChange>(flags); - CGDisplayRegisterReconfigurationCallback([](CGDirectDisplayID displayId, CGDisplayChangeSummaryFlags flags, void *userInfo) { - if (flags & kCGDisplayBeginConfigurationFlag) - return; // Wait for changes to apply + if (!beforeReconfigure) + updateScreens(); + }; + CGDisplayRegisterReconfigurationCallback(s_displayReconfigurationCallBack, nullptr); - Q_UNUSED(userInfo); + s_screenParameterObserver = QMacNotificationObserver(NSApplication.sharedApplication, + NSApplicationDidChangeScreenParametersNotification, [&]() { + qCDebug(lcQpaScreen) << "Received screen parameter change notification"; + updateScreens(); + }); +} - qCDebug(lcQpaScreen).verbosity(0).nospace() << "Display reconfiguration" - << " (" << QFlags<CoreGraphics::DisplayChange>(flags) << ")" - << " for displayId=" << displayId; +/* + Update the list of available QScreens, and the properties of existing screens. + + At this point we rely on the NSScreen.screens to be up to date. +*/ +void QCocoaScreen::updateScreens() +{ + // Adding, updating, or removing a screen below might trigger + // Qt or the application to move a window to a different screen, + // recursing back here via QCocoaWindow::windowDidChangeScreen. + // The update code is not re-entrant, so bail out if we end up + // in this situation. The screens will stabilize eventually. + static bool updatingScreens = false; + if (updatingScreens) { + qCInfo(lcQpaScreen) << "Skipping screen update, already updating"; + return; + } + QBoolBlocker recursionGuard(updatingScreens); + + uint32_t displayCount = 0; + if (CGGetOnlineDisplayList(0, nullptr, &displayCount) != kCGErrorSuccess) + qFatal("Failed to get number of online displays"); + + QVector<CGDirectDisplayID> onlineDisplays(displayCount); + if (CGGetOnlineDisplayList(displayCount, onlineDisplays.data(), &displayCount) != kCGErrorSuccess) + qFatal("Failed to get online displays"); + + qCInfo(lcQpaScreen) << "Updating screens with" << displayCount + << "online displays:" << onlineDisplays; + + // TODO: Verify whether we can always assume the main display is first + int mainDisplayIndex = onlineDisplays.indexOf(CGMainDisplayID()); + if (mainDisplayIndex < 0) { + qCWarning(lcQpaScreen) << "Main display not in list of online displays!"; + } else if (mainDisplayIndex > 0) { + qCWarning(lcQpaScreen) << "Main display not first display, making sure it is"; + onlineDisplays.move(mainDisplayIndex, 0); + } - QCocoaScreen *cocoaScreen = QCocoaScreen::get(displayId); + for (CGDirectDisplayID displayId : onlineDisplays) { + Q_ASSERT(CGDisplayIsOnline(displayId)); - if ((flags & kCGDisplayAddFlag) || !cocoaScreen) { - if (!CGDisplayIsActive(displayId)) { - qCDebug(lcQpaScreen) << "Not adding inactive display" << displayId; - return; // Will be added when activated + if (CGDisplayMirrorsDisplay(displayId)) + continue; + + // A single physical screen can map to multiple displays IDs, + // depending on which GPU is in use or which physical port the + // screen is connected to. By mapping the display ID to a UUID, + // which are shared between displays that target the same screen, + // we can pick an existing QScreen to update instead of needlessly + // adding and removing QScreens. + QCFType<CFUUIDRef> uuid = CGDisplayCreateUUIDFromDisplayID(displayId); + Q_ASSERT(uuid); + + if (QCocoaScreen *existingScreen = QCocoaScreen::get(uuid)) { + existingScreen->update(displayId); + qCInfo(lcQpaScreen) << "Updated" << existingScreen; + if (CGDisplayIsMain(displayId) && existingScreen != qGuiApp->primaryScreen()->handle()) { + qCInfo(lcQpaScreen) << "Primary screen changed to" << existingScreen; + QWindowSystemInterface::handlePrimaryScreenChanged(existingScreen); } - QCocoaScreen::add(displayId); - } else if ((flags & kCGDisplayRemoveFlag) || !CGDisplayIsActive(displayId)) { - cocoaScreen->remove(); } else { - // Detect changes to the primary screen immediately, instead of - // waiting for a display reconfigure with kCGDisplaySetMainFlag. - // This ensures that any property updates to the other screens - // will be in reference to the correct primary screen. - QCocoaScreen *mainDisplay = QCocoaScreen::get(CGMainDisplayID()); - if (QGuiApplication::primaryScreen()->handle() != mainDisplay) { - mainDisplay->updateProperties(); - qCInfo(lcQpaScreen) << "Primary screen changed to" << mainDisplay; - QWindowSystemInterface::handlePrimaryScreenChanged(mainDisplay); - if (cocoaScreen == mainDisplay) - return; // Already reconfigured - } - - cocoaScreen->updateProperties(); - qCInfo(lcQpaScreen).nospace() << "Reconfigured " << - (primaryScreen() == cocoaScreen ? "primary " : "") - << cocoaScreen; + QCocoaScreen::add(displayId); } - }, nullptr); + } + + for (QScreen *screen : QGuiApplication::screens()) { + QCocoaScreen *platformScreen = static_cast<QCocoaScreen*>(screen->handle()); + if (!platformScreen->isOnline() || platformScreen->isMirroring()) + platformScreen->remove(); + } } void QCocoaScreen::add(CGDirectDisplayID displayId) { const bool isPrimary = CGDisplayIsMain(displayId); QCocoaScreen *cocoaScreen = new QCocoaScreen(displayId); - qCInfo(lcQpaScreen).nospace() << "Adding " << (isPrimary ? "new primary " : "") << cocoaScreen; + qCInfo(lcQpaScreen) << "Adding" << cocoaScreen + << (isPrimary ? "as new primary screen" : ""); QWindowSystemInterface::handleScreenAdded(cocoaScreen, isPrimary); } QCocoaScreen::QCocoaScreen(CGDirectDisplayID displayId) : QPlatformScreen(), m_displayId(displayId) { - updateProperties(); + update(m_displayId); m_cursor = new QCocoaCursor; } @@ -146,12 +161,16 @@ void QCocoaScreen::cleanupScreens() // Remove screens in reverse order to avoid crash in case of multiple screens for (QScreen *screen : backwards(QGuiApplication::screens())) static_cast<QCocoaScreen*>(screen->handle())->remove(); + + Q_ASSERT(s_displayReconfigurationCallBack); + CGDisplayRemoveReconfigurationCallback(s_displayReconfigurationCallBack, nullptr); + s_displayReconfigurationCallBack = nullptr; + + s_screenParameterObserver.remove(); } void QCocoaScreen::remove() { - m_displayId = kCGNullDirectDisplay; // Prevent stale references during removal - // This may result in the application responding to QGuiApplication::screenRemoved // by moving the window to another screen, either by setGeometry, or by setScreen. // If the window isn't moved by the application, Qt will as a fallback move it to @@ -163,7 +182,7 @@ void QCocoaScreen::remove() // QCocoaWindow::windowDidChangeScreen. At that point the window will appear to have // already changed its screen, but that's only true if comparing the Qt screens, // not when comparing the NSScreens. - qCInfo(lcQpaScreen).nospace() << "Removing " << (primaryScreen() == this ? "current primary " : "") << this; + qCInfo(lcQpaScreen) << "Removing " << this; QWindowSystemInterface::handleScreenRemoved(this); } @@ -181,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(); @@ -191,13 +210,13 @@ static QString displayName(CGDirectDisplayID displayID) NSDictionary *info = [(__bridge NSDictionary*)IODisplayCreateInfoDictionary( display, kIODisplayOnlyPreferredName) autorelease]; - if ([[info objectForKey:@kDisplayVendorID] longValue] != CGDisplayVendorNumber(displayID)) + if ([[info objectForKey:@kDisplayVendorID] unsignedIntValue] != CGDisplayVendorNumber(displayID)) continue; - if ([[info objectForKey:@kDisplayProductID] longValue] != CGDisplayModelNumber(displayID)) + if ([[info objectForKey:@kDisplayProductID] unsignedIntValue] != CGDisplayModelNumber(displayID)) continue; - if ([[info objectForKey:@kDisplaySerialNumber] longValue] != CGDisplaySerialNumber(displayID)) + if ([[info objectForKey:@kDisplaySerialNumber] unsignedIntValue] != CGDisplaySerialNumber(displayID)) continue; NSDictionary *localizedNames = [info objectForKey:@kDisplayProductName]; @@ -210,18 +229,26 @@ static QString displayName(CGDirectDisplayID displayID) return QString(); } -void QCocoaScreen::updateProperties() +void QCocoaScreen::update(CGDirectDisplayID displayId) { - Q_ASSERT(m_displayId); + if (displayId != m_displayId) { + qCDebug(lcQpaScreen) << "Reconnecting" << this << "as display" << displayId; + m_displayId = displayId; + } - const QRect previousGeometry = m_geometry; - const QRect previousAvailableGeometry = m_availableGeometry; - const QDpi previousLogicalDpi = m_logicalDpi; - const qreal previousRefreshRate = m_refreshRate; + Q_ASSERT(isOnline()); // Some properties are only available via NSScreen NSScreen *nsScreen = nativeScreen(); - Q_ASSERT(nsScreen); + if (!nsScreen) { + qCDebug(lcQpaScreen) << "Corresponding NSScreen not yet available. Deferring update"; + return; + } + + 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())); @@ -232,24 +259,34 @@ void QCocoaScreen::updateProperties() m_format = QImage::Format_RGB32; m_depth = NSBitsPerPixelFromDepth(nsScreen.depth); + m_colorSpace = QColorSpace::fromIccProfile(QByteArray::fromNSData(nsScreen.colorSpace.ICCProfileData)); + if (!m_colorSpace.isValid()) { + qCWarning(lcQpaScreen) << "Failed to parse ICC profile for" << nsScreen.colorSpace + << "with ICC data" << nsScreen.colorSpace.ICCProfileData + << "- Falling back to sRGB"; + m_colorSpace = QColorSpace::SRgb; + } CGSize size = CGDisplayScreenSize(m_displayId); m_physicalSize = QSizeF(size.width, size.height); - m_logicalDpi.first = 72; - m_logicalDpi.second = 72; QCFType<CGDisplayModeRef> displayMode = CGDisplayCopyDisplayMode(m_displayId); float refresh = CGDisplayModeGetRefreshRate(displayMode); m_refreshRate = refresh > 0 ? refresh : 60.0; + m_rotation = CGDisplayRotation(displayId); - m_name = displayName(m_displayId); + if (@available(macOS 10.15, *)) + m_name = QString::fromNSString(nsScreen.localizedName); + else + m_name = displayName(m_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_logicalDpi != previousLogicalDpi) - QWindowSystemInterface::handleScreenLogicalDotsPerInchChange(screen(), m_logicalDpi.first, m_logicalDpi.second); if (m_refreshRate != previousRefreshRate) QWindowSystemInterface::handleScreenRefreshRateChange(screen(), m_refreshRate); } @@ -258,19 +295,33 @@ void QCocoaScreen::updateProperties() Q_LOGGING_CATEGORY(lcQpaScreenUpdates, "qt.qpa.screen.updates", QtCriticalMsg); -void QCocoaScreen::requestUpdate() +bool QCocoaScreen::requestUpdate() { Q_ASSERT(m_displayId); + if (!isOnline()) { + qCDebug(lcQpaScreenUpdates) << this << "is not online. Ignoring update request"; + return false; + } + if (!m_displayLink) { - CVDisplayLinkCreateWithCGDisplay(m_displayId, &m_displayLink); + qCDebug(lcQpaScreenUpdates) << "Creating display link for" << this; + if (CVDisplayLinkCreateWithCGDisplay(m_displayId, &m_displayLink) != kCVReturnSuccess) { + qCWarning(lcQpaScreenUpdates) << "Failed to create display link for" << this; + return false; + } + if (auto displayId = CVDisplayLinkGetCurrentCGDisplay(m_displayLink); displayId != m_displayId) { + qCWarning(lcQpaScreenUpdates) << "Unexpected display" << displayId << "for display link"; + CVDisplayLinkRelease(m_displayLink); + m_displayLink = nullptr; + return false; + } CVDisplayLinkSetOutputCallback(m_displayLink, [](CVDisplayLinkRef, const CVTimeStamp*, const CVTimeStamp*, CVOptionFlags, CVOptionFlags*, void* displayLinkContext) -> int { // FIXME: It would be nice if update requests would include timing info static_cast<QCocoaScreen*>(displayLinkContext)->deliverUpdateRequests(); return kCVReturnSuccess; }, this); - qCDebug(lcQpaScreenUpdates) << "Display link created for" << this; // During live window resizing -[NSWindow _resizeWithEvent:] will spin a local event loop // in event-tracking mode, dequeuing only the mouse drag events needed to update the window's @@ -325,6 +376,8 @@ void QCocoaScreen::requestUpdate() qCDebug(lcQpaScreenUpdates) << "Starting display link for" << this; CVDisplayLinkStart(m_displayLink); } + + return true; } // Helper to allow building up debug output in multiple steps @@ -350,8 +403,8 @@ struct DeferredDebugHelper void QCocoaScreen::deliverUpdateRequests() { - if (!m_displayId) - return; // Screen removed + if (!isOnline()) + return; QMacAutoReleasePool pool; @@ -418,6 +471,25 @@ 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 @@ -455,100 +527,153 @@ 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; - // Continue the search if the window is not a top-level window. - if (!window->isTopLevel()) - continue; + auto nativeGeometry = QHighDpi::toNativePixels(w->geometry(), w); + if (!nativeGeometry.contains(point)) + return; - // Stop searching. The current window is the correct window. - break; - } while (topWindowNumber > 0); + QRegion mask = QHighDpi::toNativeLocalPosition(w->mask(), w); + if (!mask.isEmpty() && !mask.contains(point - nativeGeometry.topLeft())) + return; + + window = w; + + // Continue the search if the window is not a top-level window + if (!window->isTopLevel()) + return; + + *stop = true; + } + ]; return window; } +/*! + \internal + + Coordinates are in screen coordinates if \a view is 0, otherwise they are in view + coordinates. +*/ QPixmap QCocoaScreen::grabWindow(WId view, int x, int y, int width, int height) const { - // Determine the grab rect. FIXME: The rect should be bounded by the view's - // geometry, but note that for the pixeltool use case that window will be the - // desktop widget's view, which currently gets resized to fit one screen - // only, since its NSWindow has the NSWindowStyleMaskTitled flag set. - Q_UNUSED(view); + /* + Grab the grabRect section of the specified display into a pixmap that has + sRGB color spec. Once Qt supports a fully color-managed flow and conversions + that don't lose the colorspec information, we would want the image to maintain + the color spec of the display from which it was grabbed. Ultimately, rendering + the returned pixmap on the same display from which it was grabbed should produce + identical visual results. + */ + auto grabFromDisplay = [](CGDirectDisplayID displayId, const QRect &grabRect) -> QPixmap { + QCFType<CGImageRef> image = CGDisplayCreateImageForRect(displayId, grabRect.toCGRect()); + const QCFType<CGColorSpaceRef> sRGBcolorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB); + if (CGImageGetColorSpace(image) != sRGBcolorSpace) { + qCDebug(lcQpaScreen) << "applying color correction for display" << displayId; + image = CGImageCreateCopyWithColorSpace(image, sRGBcolorSpace); + } + QPixmap pixmap = QPixmap::fromImage(qt_mac_toQImage(image)); + pixmap.setDevicePixelRatio(nativeScreenForDisplayId(displayId).backingScaleFactor); + return pixmap; + }; + QRect grabRect = QRect(x, y, width, height); qCDebug(lcQpaScreen) << "input grab rect" << grabRect; - // Find which displays to grab from, or all of them if the grab size is unspecified + if (!view) { + // coordinates are relative to the screen + if (!grabRect.isValid()) // entire screen + grabRect = QRect(QPoint(0, 0), geometry().size()); + else + grabRect.translate(-geometry().topLeft()); + return grabFromDisplay(displayId(), grabRect); + } + + // grab the window; grab rect in window coordinates might span multiple screens + NSView *nsView = reinterpret_cast<NSView*>(view); + NSPoint windowPoint = [nsView convertPoint:NSMakePoint(0, 0) toView:nil]; + NSRect screenRect = [nsView.window convertRectToScreen:NSMakeRect(windowPoint.x, windowPoint.y, 1, 1)]; + QPoint position = mapFromNative(screenRect.origin).toPoint(); + QSize size = QRectF::fromCGRect(NSRectToCGRect(nsView.bounds)).toRect().size(); + QRect windowRect = QRect(position, size); + if (!grabRect.isValid()) + grabRect = windowRect; + else + grabRect.translate(windowRect.topLeft()); + + // Find which displays to grab from const int maxDisplays = 128; CGDirectDisplayID displays[maxDisplays]; CGDisplayCount displayCount; - CGRect cgRect = (width < 0 || height < 0) ? CGRectInfinite : grabRect.toCGRect(); + CGRect cgRect = grabRect.isValid() ? grabRect.toCGRect() : CGRectInfinite; const CGDisplayErr err = CGGetDisplaysWithRect(cgRect, maxDisplays, displays, &displayCount); if (err || displayCount == 0) return QPixmap(); - // If the grab size is not specified, set it to be the bounding box of all screens, - if (width < 0 || height < 0) { - QRect windowRect; - for (uint i = 0; i < displayCount; ++i) { - QRect displayBounds = QRectF::fromCGRect(CGDisplayBounds(displays[i])).toRect(); - windowRect = windowRect.united(displayBounds); - } - if (grabRect.width() < 0) - grabRect.setWidth(windowRect.width()); - if (grabRect.height() < 0) - grabRect.setHeight(windowRect.height()); - } - qCDebug(lcQpaScreen) << "final grab rect" << grabRect << "from" << displayCount << "displays"; // Grab images from each display - QVector<QImage> images; + QVector<QPixmap> pixmaps; QVector<QRect> destinations; for (uint i = 0; i < displayCount; ++i) { auto display = displays[i]; - QRect displayBounds = QRectF::fromCGRect(CGDisplayBounds(display)).toRect(); - QRect grabBounds = displayBounds.intersected(grabRect); - QRect displayLocalGrabBounds = QRect(QPoint(grabBounds.topLeft() - displayBounds.topLeft()), grabBounds.size()); - QImage displayImage = qt_mac_toQImage(QCFType<CGImageRef>(CGDisplayCreateImageForRect(display, displayLocalGrabBounds.toCGRect()))); - displayImage.setDevicePixelRatio(displayImage.size().width() / displayLocalGrabBounds.size().width()); - images.append(displayImage); - QRect destBounds = QRect(QPoint(grabBounds.topLeft() - grabRect.topLeft()), grabBounds.size()); + const QRect displayBounds = QRectF::fromCGRect(CGDisplayBounds(display)).toRect(); + const QRect grabBounds = displayBounds.intersected(grabRect); + if (grabBounds.isNull()) { + destinations.append(QRect()); + pixmaps.append(QPixmap()); + continue; + } + const QRect displayLocalGrabBounds = QRect(QPoint(grabBounds.topLeft() - displayBounds.topLeft()), grabBounds.size()); + + qCDebug(lcQpaScreen) << "grab display" << i << "global" << grabBounds << "local" << displayLocalGrabBounds; + QPixmap displayPixmap = grabFromDisplay(display, displayLocalGrabBounds); + // Fast path for when grabbing from a single screen only + if (displayCount == 1) + return displayPixmap; + + qCDebug(lcQpaScreen) << "grab sub-image size" << displayPixmap.size() << "devicePixelRatio" << displayPixmap.devicePixelRatio(); + pixmaps.append(displayPixmap); + const QRect destBounds = QRect(QPoint(grabBounds.topLeft() - grabRect.topLeft()), grabBounds.size()); destinations.append(destBounds); - qCDebug(lcQpaScreen) << "grab display" << i << "global" << grabBounds << "local" << displayLocalGrabBounds - << "grab image size" << displayImage.size() << "devicePixelRatio" << displayImage.devicePixelRatio(); } // Determine the highest dpr, which becomes the dpr for the returned pixmap. qreal dpr = 1.0; for (uint i = 0; i < displayCount; ++i) - dpr = qMax(dpr, images.at(i).devicePixelRatio()); + dpr = qMax(dpr, pixmaps.at(i).devicePixelRatio()); // Allocate target pixmap and draw each screen's content qCDebug(lcQpaScreen) << "Create grap pixmap" << grabRect.size() << "at devicePixelRatio" << dpr; @@ -557,11 +682,34 @@ QPixmap QCocoaScreen::grabWindow(WId view, int x, int y, int width, int height) windowPixmap.fill(Qt::transparent); QPainter painter(&windowPixmap); for (uint i = 0; i < displayCount; ++i) - painter.drawImage(destinations.at(i), images.at(i)); + painter.drawPixmap(destinations.at(i), pixmaps.at(i)); return windowPixmap; } +bool QCocoaScreen::isOnline() const +{ + // When a display is disconnected CGDisplayIsOnline and other CGDisplay + // functions that take a displayId will not return false, but will start + // returning -1 to signal that the displayId is invalid. Some functions + // will also assert or even crash in this case, so it's important that + // we double check if a display is online before calling other functions. + int isOnline = CGDisplayIsOnline(m_displayId); + static const int kCGDisplayIsDisconnected = 0xffffffff; + return isOnline != kCGDisplayIsDisconnected && isOnline; +} + +/* + Returns true if a screen is mirroring another screen +*/ +bool QCocoaScreen::isMirroring() const +{ + if (!isOnline()) + return false; + + return CGDisplayMirrorsDisplay(m_displayId); +} + /*! The screen used as a reference for global window geometry */ @@ -586,7 +734,17 @@ QList<QPlatformScreen*> QCocoaScreen::virtualSiblings() const QCocoaScreen *QCocoaScreen::get(NSScreen *nsScreen) { - return get(nsScreen.qt_displayId); + auto displayId = nsScreen.qt_displayId; + auto *cocoaScreen = get(displayId); + if (!cocoaScreen) { + qCWarning(lcQpaScreen) << "Failed to map" << nsScreen + << "to QCocoaScreen. Doing last minute update."; + updateScreens(); + cocoaScreen = get(displayId); + if (!cocoaScreen) + qCWarning(lcQpaScreen) << "Last minute update failed!"; + } + return cocoaScreen; } QCocoaScreen *QCocoaScreen::get(CGDirectDisplayID displayId) @@ -600,26 +758,41 @@ QCocoaScreen *QCocoaScreen::get(CGDirectDisplayID displayId) return nullptr; } -NSScreen *QCocoaScreen::nativeScreen() const +QCocoaScreen *QCocoaScreen::get(CFUUIDRef uuid) { - if (!m_displayId) - return nil; // The display has been disconnected + for (QScreen *screen : QGuiApplication::screens()) { + auto *platformScreen = static_cast<QCocoaScreen*>(screen->handle()); + if (!platformScreen->isOnline()) + continue; - // A single display may have different displayIds depending on - // which GPU is in use or which physical port the display is - // connected to. By comparing UUIDs instead of display IDs we - // ensure that we always pick up the appropriate NSScreen. - QCFType<CFUUIDRef> uuid = CGDisplayCreateUUIDFromDisplayID(m_displayId); + auto displayId = platformScreen->displayId(); + QCFType<CFUUIDRef> candidateUuid(CGDisplayCreateUUIDFromDisplayID(displayId)); + Q_ASSERT(candidateUuid); - for (NSScreen *screen in [NSScreen screens]) { - if (QCFType<CFUUIDRef>(CGDisplayCreateUUIDFromDisplayID(screen.qt_displayId)) == uuid) - return screen; + if (candidateUuid == uuid) + return platformScreen; } - qCWarning(lcQpaScreen) << "Could not find NSScreen for display ID" << m_displayId; + return nullptr; +} + +NSScreen *QCocoaScreen::nativeScreenForDisplayId(CGDirectDisplayID displayId) +{ + for (NSScreen *screen in NSScreen.screens) { + if (screen.qt_displayId == displayId) + return screen; + } return nil; } +NSScreen *QCocoaScreen::nativeScreen() const +{ + if (!m_displayId) + return nil; // The display has been disconnected + + return nativeScreenForDisplayId(m_displayId); +} + CGPoint QCocoaScreen::mapToNative(const QPointF &pos, QCocoaScreen *screen) { Q_ASSERT(screen); @@ -651,21 +824,31 @@ QDebug operator<<(QDebug debug, const QCocoaScreen *screen) debug.nospace(); debug << "QCocoaScreen(" << (const void *)screen; if (screen) { - debug << ", geometry=" << screen->geometry(); + debug << ", " << screen->name(); + if (screen->isOnline()) { + if (CGDisplayIsAsleep(screen->displayId())) + debug << ", Sleeping"; + if (auto mirroring = CGDisplayMirrorsDisplay(screen->displayId())) + debug << ", mirroring=" << mirroring; + } else { + debug << ", Offline"; + } + debug << ", " << screen->geometry(); debug << ", dpr=" << screen->devicePixelRatio(); - debug << ", name=" << screen->name(); - debug << ", displayId=" << screen->m_displayId; - debug << ", native=" << screen->nativeScreen(); + debug << ", displayId=" << screen->displayId(); + + if (auto nativeScreen = screen->nativeScreen()) + debug << ", " << nativeScreen; } debug << ')'; return debug; } #endif // !QT_NO_DEBUG_STREAM -#include "qcocoascreen.moc" - QT_END_NAMESPACE +#include "qcocoascreen.moc" + @implementation NSScreen (QtExtras) - (CGDirectDisplayID)qt_displayId |