1/**************************************************************************** 2** 3** Copyright (C) 2017 The Qt Company Ltd. 4** Contact: https://www.qt.io/licensing/ 5** 6** This file is part of the plugins of the Qt Toolkit. 7** 8** $QT_BEGIN_LICENSE:LGPL$ 9** Commercial License Usage 10** Licensees holding valid commercial Qt licenses may use this file in 11** accordance with the commercial license agreement provided with the 12** Software or, alternatively, in accordance with the terms contained in 13** a written agreement between you and The Qt Company. For licensing terms 14** and conditions see https://www.qt.io/terms-conditions. For further 15** information use the contact form at https://www.qt.io/contact-us. 16** 17** GNU Lesser General Public License Usage 18** Alternatively, this file may be used under the terms of the GNU Lesser 19** General Public License version 3 as published by the Free Software 20** Foundation and appearing in the file LICENSE.LGPL3 included in the 21** packaging of this file. Please review the following information to 22** ensure the GNU Lesser General Public License version 3 requirements 23** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. 24** 25** GNU General Public License Usage 26** Alternatively, this file may be used under the terms of the GNU 27** General Public License version 2.0 or (at your option) the GNU General 28** Public license version 3 or any later version approved by the KDE Free 29** Qt Foundation. The licenses are as published by the Free Software 30** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 31** included in the packaging of this file. Please review the following 32** information to ensure the GNU General Public License requirements will 33** be met: https://www.gnu.org/licenses/gpl-2.0.html and 34** https://www.gnu.org/licenses/gpl-3.0.html. 35** 36** $QT_END_LICENSE$ 37** 38****************************************************************************/ 39 40#include "qcocoascreen.h" 41 42#include "qcocoawindow.h" 43#include "qcocoahelpers.h" 44#include "qcocoaintegration.h" 45 46#include <QtCore/qcoreapplication.h> 47#include <QtGui/private/qcoregraphics_p.h> 48 49#include <IOKit/graphics/IOGraphicsLib.h> 50 51#include <QtGui/private/qwindow_p.h> 52 53#include <QtCore/private/qeventdispatcher_cf_p.h> 54 55QT_BEGIN_NAMESPACE 56 57namespace CoreGraphics { 58 Q_NAMESPACE 59 enum DisplayChange { 60 ReconfiguredWithFlagsMissing = 0, 61 Moved = kCGDisplayMovedFlag, 62 SetMain = kCGDisplaySetMainFlag, 63 SetMode = kCGDisplaySetModeFlag, 64 Added = kCGDisplayAddFlag, 65 Removed = kCGDisplayRemoveFlag, 66 Enabled = kCGDisplayEnabledFlag, 67 Disabled = kCGDisplayDisabledFlag, 68 Mirrored = kCGDisplayMirrorFlag, 69 UnMirrored = kCGDisplayUnMirrorFlag, 70 DesktopShapeChanged = kCGDisplayDesktopShapeChangedFlag 71 }; 72 Q_ENUM_NS(DisplayChange) 73} 74 75NSArray *QCocoaScreen::s_screenConfigurationBeforeUpdate = nil; 76 77void QCocoaScreen::initializeScreens() 78{ 79 updateScreens(); 80 81 CGDisplayRegisterReconfigurationCallback([](CGDirectDisplayID displayId, CGDisplayChangeSummaryFlags flags, void *userInfo) { 82 Q_UNUSED(userInfo); 83 84 // Displays are reconfigured in batches, and we want to update our screens 85 // once a batch ends, so that all the states of the displays are up to date. 86 static int displayReconfigurationsInProgress = 0; 87 88 const bool beforeReconfigure = flags & kCGDisplayBeginConfigurationFlag; 89 qCDebug(lcQpaScreen).verbosity(0).nospace() << "Display " << displayId 90 << (beforeReconfigure ? " about to reconfigure" : " was ") 91 << QFlags<CoreGraphics::DisplayChange>(flags) 92 << " with " << displayReconfigurationsInProgress 93 << " display configuration(s) in progress"; 94 95 if (!flags) { 96 // CGDisplayRegisterReconfigurationCallback has been observed to be called 97 // with flags unset. This seems like a bug. The callback is not paired with 98 // a matching "completion" callback either, so we don't know whether to treat 99 // it as a begin or end of reconfigure. 100 return; 101 } 102 103 if (beforeReconfigure) { 104 if (!displayReconfigurationsInProgress++) { 105 // There might have been a screen reconfigure before this that 106 // we didn't process yet, so do that now if that's the case. 107 updateScreensIfNeeded(); 108 109 Q_ASSERT(!s_screenConfigurationBeforeUpdate); 110 s_screenConfigurationBeforeUpdate = NSScreen.screens; 111 qCDebug(lcQpaScreen, "Display reconfigure transaction started" 112 " with screen configuration %p", s_screenConfigurationBeforeUpdate); 113 114 static void (^tryScreenUpdate)(); 115 tryScreenUpdate = ^void () { 116 qCDebug(lcQpaScreen) << "Attempting screen update from runloop block"; 117 if (!updateScreensIfNeeded()) 118 CFRunLoopPerformBlock(CFRunLoopGetMain(), kCFRunLoopCommonModes, tryScreenUpdate); 119 }; 120 CFRunLoopPerformBlock(CFRunLoopGetMain(), kCFRunLoopCommonModes, tryScreenUpdate); 121 } 122 } else { 123 Q_ASSERT_X(displayReconfigurationsInProgress, "QCococaScreen", 124 "Display configuration transactions are expected to be balanced"); 125 126 if (!--displayReconfigurationsInProgress) { 127 qCDebug(lcQpaScreen) << "Display reconfigure transaction completed"; 128 // We optimistically update now, in case the NSScreens have changed 129 updateScreensIfNeeded(); 130 } 131 } 132 }, nullptr); 133 134 static QMacNotificationObserver screenParameterObserver(NSApplication.sharedApplication, 135 NSApplicationDidChangeScreenParametersNotification, [&]() { 136 qCDebug(lcQpaScreen) << "Received screen parameter change notification"; 137 updateScreensIfNeeded(); // As a last resort we update screens here 138 }); 139} 140 141bool QCocoaScreen::updateScreensIfNeeded() 142{ 143 if (!s_screenConfigurationBeforeUpdate) { 144 qCDebug(lcQpaScreen) << "QScreens have already been updated, all good"; 145 return true; 146 } 147 148 if (s_screenConfigurationBeforeUpdate == NSScreen.screens) { 149 qCDebug(lcQpaScreen) << "Still waiting for NSScreen configuration change"; 150 return false; 151 } 152 153 qCDebug(lcQpaScreen, "NSScreen configuration changed to %p", NSScreen.screens); 154 updateScreens(); 155 156 s_screenConfigurationBeforeUpdate = nil; 157 return true; 158} 159 160/* 161 Update the list of available QScreens, and the properties of existing screens. 162 163 At this point we rely on the NSScreen.screens to be up to date. 164*/ 165void QCocoaScreen::updateScreens() 166{ 167 uint32_t displayCount = 0; 168 if (CGGetOnlineDisplayList(0, nullptr, &displayCount) != kCGErrorSuccess) 169 qFatal("Failed to get number of online displays"); 170 171 QVector<CGDirectDisplayID> onlineDisplays(displayCount); 172 if (CGGetOnlineDisplayList(displayCount, onlineDisplays.data(), &displayCount) != kCGErrorSuccess) 173 qFatal("Failed to get online displays"); 174 175 qCInfo(lcQpaScreen) << "Updating screens with" << displayCount 176 << "online displays:" << onlineDisplays; 177 178 // TODO: Verify whether we can always assume the main display is first 179 int mainDisplayIndex = onlineDisplays.indexOf(CGMainDisplayID()); 180 if (mainDisplayIndex < 0) { 181 qCWarning(lcQpaScreen) << "Main display not in list of online displays!"; 182 } else if (mainDisplayIndex > 0) { 183 qCWarning(lcQpaScreen) << "Main display not first display, making sure it is"; 184 onlineDisplays.move(mainDisplayIndex, 0); 185 } 186 187 for (CGDirectDisplayID displayId : onlineDisplays) { 188 Q_ASSERT(CGDisplayIsOnline(displayId)); 189 190 if (CGDisplayMirrorsDisplay(displayId)) 191 continue; 192 193 // A single physical screen can map to multiple displays IDs, 194 // depending on which GPU is in use or which physical port the 195 // screen is connected to. By mapping the display ID to a UUID, 196 // which are shared between displays that target the same screen, 197 // we can pick an existing QScreen to update instead of needlessly 198 // adding and removing QScreens. 199 QCFType<CFUUIDRef> uuid = CGDisplayCreateUUIDFromDisplayID(displayId); 200 Q_ASSERT(uuid); 201 202 if (QCocoaScreen *existingScreen = QCocoaScreen::get(uuid)) { 203 existingScreen->update(displayId); 204 qCInfo(lcQpaScreen) << "Updated" << existingScreen; 205 if (CGDisplayIsMain(displayId) && existingScreen != qGuiApp->primaryScreen()->handle()) { 206 qCInfo(lcQpaScreen) << "Primary screen changed to" << existingScreen; 207 QWindowSystemInterface::handlePrimaryScreenChanged(existingScreen); 208 } 209 } else { 210 QCocoaScreen::add(displayId); 211 } 212 } 213 214 for (QScreen *screen : QGuiApplication::screens()) { 215 QCocoaScreen *platformScreen = static_cast<QCocoaScreen*>(screen->handle()); 216 if (!platformScreen->isOnline() || platformScreen->isMirroring()) 217 platformScreen->remove(); 218 } 219} 220 221void QCocoaScreen::add(CGDirectDisplayID displayId) 222{ 223 const bool isPrimary = CGDisplayIsMain(displayId); 224 QCocoaScreen *cocoaScreen = new QCocoaScreen(displayId); 225 qCInfo(lcQpaScreen) << "Adding" << cocoaScreen 226 << (isPrimary ? "as new primary screen" : ""); 227 QWindowSystemInterface::handleScreenAdded(cocoaScreen, isPrimary); 228} 229 230QCocoaScreen::QCocoaScreen(CGDirectDisplayID displayId) 231 : QPlatformScreen(), m_displayId(displayId) 232{ 233 update(m_displayId); 234 m_cursor = new QCocoaCursor; 235} 236 237void QCocoaScreen::cleanupScreens() 238{ 239 // Remove screens in reverse order to avoid crash in case of multiple screens 240 for (QScreen *screen : backwards(QGuiApplication::screens())) 241 static_cast<QCocoaScreen*>(screen->handle())->remove(); 242} 243 244void QCocoaScreen::remove() 245{ 246 // This may result in the application responding to QGuiApplication::screenRemoved 247 // by moving the window to another screen, either by setGeometry, or by setScreen. 248 // If the window isn't moved by the application, Qt will as a fallback move it to 249 // the primary screen via setScreen. Due to the way setScreen works, this won't 250 // actually recreate the window on the new screen, it will just assign the new 251 // QScreen to the window. The associated NSWindow will have an NSScreen determined 252 // by AppKit. AppKit will then move the window to another screen by changing the 253 // geometry, and we will get a callback in QCocoaWindow::windowDidMove and then 254 // QCocoaWindow::windowDidChangeScreen. At that point the window will appear to have 255 // already changed its screen, but that's only true if comparing the Qt screens, 256 // not when comparing the NSScreens. 257 qCInfo(lcQpaScreen) << "Removing " << this; 258 QWindowSystemInterface::handleScreenRemoved(this); 259} 260 261QCocoaScreen::~QCocoaScreen() 262{ 263 Q_ASSERT_X(!screen(), "QCocoaScreen", "QScreen should be deleted first"); 264 265 delete m_cursor; 266 267 CVDisplayLinkRelease(m_displayLink); 268 if (m_displayLinkSource) 269 dispatch_release(m_displayLinkSource); 270} 271 272static QString displayName(CGDirectDisplayID displayID) 273{ 274 QIOType<io_iterator_t> iterator; 275 if (IOServiceGetMatchingServices(kIOMasterPortDefault, 276 IOServiceMatching("IODisplayConnect"), &iterator)) 277 return QString(); 278 279 QIOType<io_service_t> display; 280 while ((display = IOIteratorNext(iterator)) != 0) 281 { 282 NSDictionary *info = [(__bridge NSDictionary*)IODisplayCreateInfoDictionary( 283 display, kIODisplayOnlyPreferredName) autorelease]; 284 285 if ([[info objectForKey:@kDisplayVendorID] longValue] != CGDisplayVendorNumber(displayID)) 286 continue; 287 288 if ([[info objectForKey:@kDisplayProductID] longValue] != CGDisplayModelNumber(displayID)) 289 continue; 290 291 if ([[info objectForKey:@kDisplaySerialNumber] longValue] != CGDisplaySerialNumber(displayID)) 292 continue; 293 294 NSDictionary *localizedNames = [info objectForKey:@kDisplayProductName]; 295 if (![localizedNames count]) 296 break; // Correct screen, but no name in dictionary 297 298 return QString::fromNSString([localizedNames objectForKey:[[localizedNames allKeys] objectAtIndex:0]]); 299 } 300 301 return QString(); 302} 303 304void QCocoaScreen::update(CGDirectDisplayID displayId) 305{ 306 if (displayId != m_displayId) { 307 qCDebug(lcQpaScreen) << "Reconnecting" << this << "as display" << displayId; 308 m_displayId = displayId; 309 } 310 311 Q_ASSERT(isOnline()); 312 313 const QRect previousGeometry = m_geometry; 314 const QRect previousAvailableGeometry = m_availableGeometry; 315 const QDpi previousLogicalDpi = m_logicalDpi; 316 const qreal previousRefreshRate = m_refreshRate; 317 318 // Some properties are only available via NSScreen 319 NSScreen *nsScreen = nativeScreen(); 320 Q_ASSERT(nsScreen); 321 322 // The reference screen for the geometry is always the primary screen 323 QRectF primaryScreenGeometry = QRectF::fromCGRect(CGDisplayBounds(CGMainDisplayID())); 324 m_geometry = qt_mac_flip(QRectF::fromCGRect(nsScreen.frame), primaryScreenGeometry).toRect(); 325 m_availableGeometry = qt_mac_flip(QRectF::fromCGRect(nsScreen.visibleFrame), primaryScreenGeometry).toRect(); 326 327 m_devicePixelRatio = nsScreen.backingScaleFactor; 328 329 m_format = QImage::Format_RGB32; 330 m_depth = NSBitsPerPixelFromDepth(nsScreen.depth); 331 332 CGSize size = CGDisplayScreenSize(m_displayId); 333 m_physicalSize = QSizeF(size.width, size.height); 334 m_logicalDpi.first = 72; 335 m_logicalDpi.second = 72; 336 337 QCFType<CGDisplayModeRef> displayMode = CGDisplayCopyDisplayMode(m_displayId); 338 float refresh = CGDisplayModeGetRefreshRate(displayMode); 339 m_refreshRate = refresh > 0 ? refresh : 60.0; 340 341 m_name = displayName(m_displayId); 342 343 const bool didChangeGeometry = m_geometry != previousGeometry || m_availableGeometry != previousAvailableGeometry; 344 345 if (didChangeGeometry) 346 QWindowSystemInterface::handleScreenGeometryChange(screen(), geometry(), availableGeometry()); 347 if (m_logicalDpi != previousLogicalDpi) 348 QWindowSystemInterface::handleScreenLogicalDotsPerInchChange(screen(), m_logicalDpi.first, m_logicalDpi.second); 349 if (m_refreshRate != previousRefreshRate) 350 QWindowSystemInterface::handleScreenRefreshRateChange(screen(), m_refreshRate); 351} 352 353// ----------------------- Display link ----------------------- 354 355Q_LOGGING_CATEGORY(lcQpaScreenUpdates, "qt.qpa.screen.updates", QtCriticalMsg); 356 357void QCocoaScreen::requestUpdate() 358{ 359 Q_ASSERT(m_displayId); 360 361 if (!m_displayLink) { 362 CVDisplayLinkCreateWithCGDisplay(m_displayId, &m_displayLink); 363 CVDisplayLinkSetOutputCallback(m_displayLink, [](CVDisplayLinkRef, const CVTimeStamp*, 364 const CVTimeStamp*, CVOptionFlags, CVOptionFlags*, void* displayLinkContext) -> int { 365 // FIXME: It would be nice if update requests would include timing info 366 static_cast<QCocoaScreen*>(displayLinkContext)->deliverUpdateRequests(); 367 return kCVReturnSuccess; 368 }, this); 369 qCDebug(lcQpaScreenUpdates) << "Display link created for" << this; 370 371 // During live window resizing -[NSWindow _resizeWithEvent:] will spin a local event loop 372 // in event-tracking mode, dequeuing only the mouse drag events needed to update the window's 373 // frame. It will repeatedly spin this loop until no longer receiving any mouse drag events, 374 // and will then update the frame (effectively coalescing/compressing the events). Unfortunately 375 // the events are pulled out using -[NSApplication nextEventMatchingEventMask:untilDate:inMode:dequeue:] 376 // which internally uses CFRunLoopRunSpecific, so the event loop will also process GCD queues and other 377 // runloop sources that have been added to the tracking mode. This includes the GCD display-link 378 // source that we use to marshal the display-link callback over to the main thread. If the 379 // subsequent delivery of the update-request on the main thread stalls due to inefficient 380 // user code, the NSEventThread will have had time to deliver additional mouse drag events, 381 // and the logic in -[NSWindow _resizeWithEvent:] will keep on compressing events and never 382 // get to the point of actually updating the window frame, making it seem like the window 383 // is stuck in its original size. Only when the user stops moving their mouse, and the event 384 // queue is completely drained of drag events, will the window frame be updated. 385 386 // By keeping an event tap listening for drag events, registered as a version 1 runloop source, 387 // we prevent the GCD source from being prioritized, giving the resize logic enough time 388 // to finish coalescing the events. This is incidental, but conveniently gives us the behavior 389 // we are looking for, interleaving display-link updates and resize events. 390 static CFMachPortRef eventTap = []() { 391 CFMachPortRef eventTap = CGEventTapCreateForPid(getpid(), kCGTailAppendEventTap, 392 kCGEventTapOptionListenOnly, NSEventMaskLeftMouseDragged, 393 [](CGEventTapProxy, CGEventType type, CGEventRef event, void *) -> CGEventRef { 394 if (type == kCGEventTapDisabledByTimeout) 395 qCWarning(lcQpaScreenUpdates) << "Event tap disabled due to timeout!"; 396 return event; // Listen only tap, so what we return doesn't really matter 397 }, nullptr); 398 CGEventTapEnable(eventTap, false); // Event taps are normally enabled when created 399 static CFRunLoopSourceRef runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0); 400 CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, kCFRunLoopCommonModes); 401 402 NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; 403 [center addObserverForName:NSWindowWillStartLiveResizeNotification object:nil queue:nil 404 usingBlock:^(NSNotification *notification) { 405 qCDebug(lcQpaScreenUpdates) << "Live resize of" << notification.object 406 << "started. Enabling event tap"; 407 CGEventTapEnable(eventTap, true); 408 }]; 409 [center addObserverForName:NSWindowDidEndLiveResizeNotification object:nil queue:nil 410 usingBlock:^(NSNotification *notification) { 411 qCDebug(lcQpaScreenUpdates) << "Live resize of" << notification.object 412 << "ended. Disabling event tap"; 413 CGEventTapEnable(eventTap, false); 414 }]; 415 return eventTap; 416 }(); 417 Q_UNUSED(eventTap); 418 } 419 420 if (!CVDisplayLinkIsRunning(m_displayLink)) { 421 qCDebug(lcQpaScreenUpdates) << "Starting display link for" << this; 422 CVDisplayLinkStart(m_displayLink); 423 } 424} 425 426// Helper to allow building up debug output in multiple steps 427struct DeferredDebugHelper 428{ 429 DeferredDebugHelper(const QLoggingCategory &cat) { 430 if (cat.isDebugEnabled()) 431 debug = new QDebug(QMessageLogger().debug(cat).nospace()); 432 } 433 ~DeferredDebugHelper() { 434 flushOutput(); 435 } 436 void flushOutput() { 437 if (debug) { 438 delete debug; 439 debug = nullptr; 440 } 441 } 442 QDebug *debug = nullptr; 443}; 444 445#define qDeferredDebug(helper) if (Q_UNLIKELY(helper.debug)) *helper.debug 446 447void QCocoaScreen::deliverUpdateRequests() 448{ 449 if (!isOnline()) 450 return; 451 452 QMacAutoReleasePool pool; 453 454 // The CVDisplayLink callback is a notification that it's a good time to produce a new frame. 455 // Since the callback is delivered on a separate thread we have to marshal it over to the 456 // main thread, as Qt requires update requests to be delivered there. This needs to happen 457 // asynchronously, as otherwise we may end up deadlocking if the main thread calls back 458 // into any of the CVDisplayLink APIs. 459 if (!NSThread.isMainThread) { 460 // We're explicitly not using the data of the GCD source to track the pending updates, 461 // as the data isn't reset to 0 until after the event handler, and also doesn't update 462 // during the event handler, both of which we need to track late frames. 463 const int pendingUpdates = ++m_pendingUpdates; 464 465 DeferredDebugHelper screenUpdates(lcQpaScreenUpdates()); 466 qDeferredDebug(screenUpdates) << "display link callback for screen " << m_displayId; 467 468 if (const int framesAheadOfDelivery = pendingUpdates - 1) { 469 // If we have more than one update pending it means that a previous display link callback 470 // has not been fully processed on the main thread, either because GCD hasn't delivered 471 // it on the main thread yet, because the processing of the update request is taking 472 // too long, or because the update request was deferred due to window live resizing. 473 qDeferredDebug(screenUpdates) << ", " << framesAheadOfDelivery << " frame(s) ahead"; 474 } 475 476 qDeferredDebug(screenUpdates) << "; signaling dispatch source"; 477 478 if (!m_displayLinkSource) { 479 m_displayLinkSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue()); 480 dispatch_source_set_event_handler(m_displayLinkSource, ^{ 481 deliverUpdateRequests(); 482 }); 483 dispatch_resume(m_displayLinkSource); 484 } 485 486 dispatch_source_merge_data(m_displayLinkSource, 1); 487 488 } else { 489 DeferredDebugHelper screenUpdates(lcQpaScreenUpdates()); 490 qDeferredDebug(screenUpdates) << "gcd event handler on main thread"; 491 492 const int pendingUpdates = m_pendingUpdates; 493 if (pendingUpdates > 1) 494 qDeferredDebug(screenUpdates) << ", " << (pendingUpdates - 1) << " frame(s) behind display link"; 495 496 screenUpdates.flushOutput(); 497 498 bool pauseUpdates = true; 499 500 auto windows = QGuiApplication::allWindows(); 501 for (int i = 0; i < windows.size(); ++i) { 502 QWindow *window = windows.at(i); 503 auto *platformWindow = static_cast<QCocoaWindow*>(window->handle()); 504 if (!platformWindow) 505 continue; 506 507 if (!platformWindow->hasPendingUpdateRequest()) 508 continue; 509 510 if (window->screen() != screen()) 511 continue; 512 513 // Skip windows that are not doing update requests via display link 514 if (!platformWindow->updatesWithDisplayLink()) 515 continue; 516 517 platformWindow->deliverUpdateRequest(); 518 519 // Another update request was triggered, keep the display link running 520 if (platformWindow->hasPendingUpdateRequest()) 521 pauseUpdates = false; 522 } 523 524 if (pauseUpdates) { 525 // Pause the display link if there are no pending update requests 526 qCDebug(lcQpaScreenUpdates) << "Stopping display link for" << this; 527 CVDisplayLinkStop(m_displayLink); 528 } 529 530 if (const int missedUpdates = m_pendingUpdates.fetchAndStoreRelaxed(0) - pendingUpdates) { 531 qCWarning(lcQpaScreenUpdates) << "main thread missed" << missedUpdates 532 << "update(s) from display link during update request delivery"; 533 } 534 } 535} 536 537bool QCocoaScreen::isRunningDisplayLink() const 538{ 539 return m_displayLink && CVDisplayLinkIsRunning(m_displayLink); 540} 541 542// ----------------------------------------------------------- 543 544QPlatformScreen::SubpixelAntialiasingType QCocoaScreen::subpixelAntialiasingTypeHint() const 545{ 546 QPlatformScreen::SubpixelAntialiasingType type = QPlatformScreen::subpixelAntialiasingTypeHint(); 547 if (type == QPlatformScreen::Subpixel_None) { 548 // Every OSX machine has RGB pixels unless a peculiar or rotated non-Apple screen is attached 549 type = QPlatformScreen::Subpixel_RGB; 550 } 551 return type; 552} 553 554QWindow *QCocoaScreen::topLevelAt(const QPoint &point) const 555{ 556 NSPoint screenPoint = mapToNative(point); 557 558 // Search (hit test) for the top-level window. [NSWidow windowNumberAtPoint: 559 // belowWindowWithWindowNumber] may return windows that are not interesting 560 // to Qt. The search iterates until a suitable window or no window is found. 561 NSInteger topWindowNumber = 0; 562 QWindow *window = nullptr; 563 do { 564 // Get the top-most window, below any previously rejected window. 565 topWindowNumber = [NSWindow windowNumberAtPoint:screenPoint 566 belowWindowWithWindowNumber:topWindowNumber]; 567 568 // Continue the search if the window does not belong to this process. 569 NSWindow *nsWindow = [NSApp windowWithWindowNumber:topWindowNumber]; 570 if (!nsWindow) 571 continue; 572 573 // Continue the search if the window does not belong to Qt. 574 if (![nsWindow conformsToProtocol:@protocol(QNSWindowProtocol)]) 575 continue; 576 577 QCocoaWindow *cocoaWindow = qnsview_cast(nsWindow.contentView).platformWindow; 578 if (!cocoaWindow) 579 continue; 580 window = cocoaWindow->window(); 581 582 // Continue the search if the window is not a top-level window. 583 if (!window->isTopLevel()) 584 continue; 585 586 // Stop searching. The current window is the correct window. 587 break; 588 } while (topWindowNumber > 0); 589 590 return window; 591} 592 593QPixmap QCocoaScreen::grabWindow(WId view, int x, int y, int width, int height) const 594{ 595 // Determine the grab rect. FIXME: The rect should be bounded by the view's 596 // geometry, but note that for the pixeltool use case that window will be the 597 // desktop widget's view, which currently gets resized to fit one screen 598 // only, since its NSWindow has the NSWindowStyleMaskTitled flag set. 599 Q_UNUSED(view); 600 QRect grabRect = QRect(x, y, width, height); 601 qCDebug(lcQpaScreen) << "input grab rect" << grabRect; 602 603 // Find which displays to grab from, or all of them if the grab size is unspecified 604 const int maxDisplays = 128; 605 CGDirectDisplayID displays[maxDisplays]; 606 CGDisplayCount displayCount; 607 CGRect cgRect = (width < 0 || height < 0) ? CGRectInfinite : grabRect.toCGRect(); 608 const CGDisplayErr err = CGGetDisplaysWithRect(cgRect, maxDisplays, displays, &displayCount); 609 if (err || displayCount == 0) 610 return QPixmap(); 611 612 // If the grab size is not specified, set it to be the bounding box of all screens, 613 if (width < 0 || height < 0) { 614 QRect windowRect; 615 for (uint i = 0; i < displayCount; ++i) { 616 QRect displayBounds = QRectF::fromCGRect(CGDisplayBounds(displays[i])).toRect(); 617 // Only include the screen if it is positioned past the x/y position 618 if ((displayBounds.x() >= x || displayBounds.right() > x) && 619 (displayBounds.y() >= y || displayBounds.bottom() > y)) { 620 windowRect = windowRect.united(displayBounds); 621 } 622 } 623 if (grabRect.width() < 0) 624 grabRect.setWidth(windowRect.width()); 625 if (grabRect.height() < 0) 626 grabRect.setHeight(windowRect.height()); 627 } 628 629 qCDebug(lcQpaScreen) << "final grab rect" << grabRect << "from" << displayCount << "displays"; 630 631 // Grab images from each display 632 QVector<QImage> images; 633 QVector<QRect> destinations; 634 for (uint i = 0; i < displayCount; ++i) { 635 auto display = displays[i]; 636 QRect displayBounds = QRectF::fromCGRect(CGDisplayBounds(display)).toRect(); 637 QRect grabBounds = displayBounds.intersected(grabRect); 638 if (grabBounds.isNull()) { 639 destinations.append(QRect()); 640 images.append(QImage()); 641 continue; 642 } 643 QRect displayLocalGrabBounds = QRect(QPoint(grabBounds.topLeft() - displayBounds.topLeft()), grabBounds.size()); 644 QImage displayImage = qt_mac_toQImage(QCFType<CGImageRef>(CGDisplayCreateImageForRect(display, displayLocalGrabBounds.toCGRect()))); 645 displayImage.setDevicePixelRatio(displayImage.size().width() / displayLocalGrabBounds.size().width()); 646 images.append(displayImage); 647 QRect destBounds = QRect(QPoint(grabBounds.topLeft() - grabRect.topLeft()), grabBounds.size()); 648 destinations.append(destBounds); 649 qCDebug(lcQpaScreen) << "grab display" << i << "global" << grabBounds << "local" << displayLocalGrabBounds 650 << "grab image size" << displayImage.size() << "devicePixelRatio" << displayImage.devicePixelRatio(); 651 } 652 653 // Determine the highest dpr, which becomes the dpr for the returned pixmap. 654 qreal dpr = 1.0; 655 for (uint i = 0; i < displayCount; ++i) 656 dpr = qMax(dpr, images.at(i).devicePixelRatio()); 657 658 // Allocate target pixmap and draw each screen's content 659 qCDebug(lcQpaScreen) << "Create grap pixmap" << grabRect.size() << "at devicePixelRatio" << dpr; 660 QPixmap windowPixmap(grabRect.size() * dpr); 661 windowPixmap.setDevicePixelRatio(dpr); 662 windowPixmap.fill(Qt::transparent); 663 QPainter painter(&windowPixmap); 664 for (uint i = 0; i < displayCount; ++i) 665 painter.drawImage(destinations.at(i), images.at(i)); 666 667 return windowPixmap; 668} 669 670bool QCocoaScreen::isOnline() const 671{ 672 // When a display is disconnected CGDisplayIsOnline and other CGDisplay 673 // functions that take a displayId will not return false, but will start 674 // returning -1 to signal that the displayId is invalid. Some functions 675 // will also assert or even crash in this case, so it's important that 676 // we double check if a display is online before calling other functions. 677 auto isOnline = CGDisplayIsOnline(m_displayId); 678 static const uint32_t kCGDisplayIsDisconnected = int32_t(-1); 679 return isOnline != kCGDisplayIsDisconnected && isOnline; 680} 681 682/* 683 Returns true if a screen is mirroring another screen 684*/ 685bool QCocoaScreen::isMirroring() const 686{ 687 if (!isOnline()) 688 return false; 689 690 return CGDisplayMirrorsDisplay(m_displayId); 691} 692 693/*! 694 The screen used as a reference for global window geometry 695*/ 696QCocoaScreen *QCocoaScreen::primaryScreen() 697{ 698 // Note: The primary screen that Qt knows about may not match the current CGMainDisplayID() 699 // if macOS has not yet been able to inform us that the main display has changed, but we 700 // will update the primary screen accordingly once the reconfiguration callback comes in. 701 return static_cast<QCocoaScreen *>(QGuiApplication::primaryScreen()->handle()); 702} 703 704QList<QPlatformScreen*> QCocoaScreen::virtualSiblings() const 705{ 706 QList<QPlatformScreen*> siblings; 707 708 // Screens on macOS are always part of the same virtual desktop 709 for (QScreen *screen : QGuiApplication::screens()) 710 siblings << screen->handle(); 711 712 return siblings; 713} 714 715QCocoaScreen *QCocoaScreen::get(NSScreen *nsScreen) 716{ 717 if (s_screenConfigurationBeforeUpdate) { 718 qCWarning(lcQpaScreen) << "Trying to resolve screen while waiting for screen reconfigure!"; 719 if (!updateScreensIfNeeded()) 720 qCWarning(lcQpaScreen) << "Failed to do last minute screen update. Expect crashes."; 721 } 722 723 return get(nsScreen.qt_displayId); 724} 725 726QCocoaScreen *QCocoaScreen::get(CGDirectDisplayID displayId) 727{ 728 for (QScreen *screen : QGuiApplication::screens()) { 729 QCocoaScreen *cocoaScreen = static_cast<QCocoaScreen*>(screen->handle()); 730 if (cocoaScreen->m_displayId == displayId) 731 return cocoaScreen; 732 } 733 734 return nullptr; 735} 736 737QCocoaScreen *QCocoaScreen::get(CFUUIDRef uuid) 738{ 739 for (QScreen *screen : QGuiApplication::screens()) { 740 auto *platformScreen = static_cast<QCocoaScreen*>(screen->handle()); 741 if (!platformScreen->isOnline()) 742 continue; 743 744 auto displayId = platformScreen->displayId(); 745 QCFType<CFUUIDRef> candidateUuid(CGDisplayCreateUUIDFromDisplayID(displayId)); 746 Q_ASSERT(candidateUuid); 747 748 if (candidateUuid == uuid) 749 return platformScreen; 750 } 751 752 return nullptr; 753} 754 755NSScreen *QCocoaScreen::nativeScreen() const 756{ 757 if (!m_displayId) 758 return nil; // The display has been disconnected 759 760 for (NSScreen *screen in NSScreen.screens) { 761 if (screen.qt_displayId == m_displayId) 762 return screen; 763 } 764 765 return nil; 766} 767 768CGPoint QCocoaScreen::mapToNative(const QPointF &pos, QCocoaScreen *screen) 769{ 770 Q_ASSERT(screen); 771 return qt_mac_flip(pos, screen->geometry()).toCGPoint(); 772} 773 774CGRect QCocoaScreen::mapToNative(const QRectF &rect, QCocoaScreen *screen) 775{ 776 Q_ASSERT(screen); 777 return qt_mac_flip(rect, screen->geometry()).toCGRect(); 778} 779 780QPointF QCocoaScreen::mapFromNative(CGPoint pos, QCocoaScreen *screen) 781{ 782 Q_ASSERT(screen); 783 return qt_mac_flip(QPointF::fromCGPoint(pos), screen->geometry()); 784} 785 786QRectF QCocoaScreen::mapFromNative(CGRect rect, QCocoaScreen *screen) 787{ 788 Q_ASSERT(screen); 789 return qt_mac_flip(QRectF::fromCGRect(rect), screen->geometry()); 790} 791 792#ifndef QT_NO_DEBUG_STREAM 793QDebug operator<<(QDebug debug, const QCocoaScreen *screen) 794{ 795 QDebugStateSaver saver(debug); 796 debug.nospace(); 797 debug << "QCocoaScreen(" << (const void *)screen; 798 if (screen) { 799 debug << ", " << screen->name(); 800 if (screen->isOnline()) { 801 if (CGDisplayIsAsleep(screen->displayId())) 802 debug << ", Sleeping"; 803 if (auto mirroring = CGDisplayMirrorsDisplay(screen->displayId())) 804 debug << ", mirroring=" << mirroring; 805 } else { 806 debug << ", Offline"; 807 } 808 debug << ", " << screen->geometry(); 809 debug << ", dpr=" << screen->devicePixelRatio(); 810 debug << ", displayId=" << screen->displayId(); 811 812 if (auto nativeScreen = screen->nativeScreen()) 813 debug << ", " << nativeScreen; 814 } 815 debug << ')'; 816 return debug; 817} 818#endif // !QT_NO_DEBUG_STREAM 819 820#include "qcocoascreen.moc" 821 822QT_END_NAMESPACE 823 824@implementation NSScreen (QtExtras) 825 826- (CGDirectDisplayID)qt_displayId 827{ 828 return [self.deviceDescription[@"NSScreenNumber"] unsignedIntValue]; 829} 830 831@end 832