Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qcocoascreen.mm
Go to the documentation of this file.
1// Copyright (C) 2017 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:significant reason:default
4
5#include <AppKit/AppKit.h>
6
7#include "qcocoascreen.h"
8
9#include "qcocoawindow.h"
10#include "qcocoahelpers.h"
12
13#include <QtCore/qcoreapplication.h>
14#include <QtGui/private/qcoregraphics_p.h>
15
16#include <IOKit/graphics/IOGraphicsLib.h>
17
18#include <QtGui/private/qwindow_p.h>
19#include <QtGui/private/qhighdpiscaling_p.h>
20
21#include <QtCore/private/qcore_mac_p.h>
22#include <QtCore/private/qeventdispatcher_cf_p.h>
23
24QT_BEGIN_NAMESPACE
25
43
44QMacNotificationObserver QCocoaScreen::s_screenParameterObserver;
45CGDisplayReconfigurationCallBack QCocoaScreen::s_displayReconfigurationCallBack = nullptr;
46
47void QCocoaScreen::initializeScreens()
48{
49 updateScreens();
50
51 s_displayReconfigurationCallBack = [](CGDirectDisplayID displayId, CGDisplayChangeSummaryFlags flags, void *userInfo) {
52 Q_UNUSED(userInfo);
53
54 const bool beforeReconfigure = flags & kCGDisplayBeginConfigurationFlag;
55 qCDebug(lcQpaScreen).verbosity(0) << "Display" << displayId
56 << (beforeReconfigure ? "beginning" : "finished") << "reconfigure"
57 << QFlags<CoreGraphics::DisplayChange>(flags);
58
59 if (!beforeReconfigure)
60 updateScreens();
61 };
62 CGDisplayRegisterReconfigurationCallback(s_displayReconfigurationCallBack, nullptr);
63
64 s_screenParameterObserver = QMacNotificationObserver(NSApplication.sharedApplication,
65 NSApplicationDidChangeScreenParametersNotification, [&]() {
66 qCDebug(lcQpaScreen) << "Received screen parameter change notification";
67 updateScreens();
68
69 // The notification is posted when the EDR headroom of a display changes,
70 // which might affect the rendering of windows that opt in to EDR.
71 updateHdrWindows();
72 });
73}
74
75/*
76 Update the list of available QScreens, and the properties of existing screens.
77
78 At this point we rely on the NSScreen.screens to be up to date.
79*/
80void QCocoaScreen::updateScreens()
81{
82 // Adding, updating, or removing a screen below might trigger
83 // Qt or the application to move a window to a different screen,
84 // recursing back here via QCocoaWindow::windowDidChangeScreen.
85 // The update code is not re-entrant, so bail out if we end up
86 // in this situation. The screens will stabilize eventually.
87 static bool updatingScreens = false;
88 if (updatingScreens) {
89 qCInfo(lcQpaScreen) << "Skipping screen update, already updating";
90 return;
91 }
92 QScopedValueRollback recursionGuard(updatingScreens, true);
93
94 uint32_t displayCount = 0;
95 if (CGGetOnlineDisplayList(0, nullptr, &displayCount) != kCGErrorSuccess)
96 qFatal("Failed to get number of online displays");
97
98 QVector<CGDirectDisplayID> onlineDisplays(displayCount);
99 if (CGGetOnlineDisplayList(displayCount, onlineDisplays.data(), &displayCount) != kCGErrorSuccess)
100 qFatal("Failed to get online displays");
101
102 qCInfo(lcQpaScreen) << "Updating screens with" << displayCount
103 << "online displays:" << onlineDisplays;
104
105 // TODO: Verify whether we can always assume the main display is first
106 int mainDisplayIndex = onlineDisplays.indexOf(CGMainDisplayID());
107 if (mainDisplayIndex < 0) {
108 qCWarning(lcQpaScreen) << "Main display not in list of online displays!";
109 } else if (mainDisplayIndex > 0) {
110 qCWarning(lcQpaScreen) << "Main display not first display, making sure it is";
111 onlineDisplays.move(mainDisplayIndex, 0);
112 }
113
114 for (CGDirectDisplayID displayId : onlineDisplays) {
115 Q_ASSERT(CGDisplayIsOnline(displayId));
116
117 if (CGDisplayMirrorsDisplay(displayId))
118 continue;
119
120 // A single physical screen can map to multiple displays IDs,
121 // depending on which GPU is in use or which physical port the
122 // screen is connected to. By mapping the display ID to a UUID,
123 // which are shared between displays that target the same screen,
124 // we can pick an existing QScreen to update instead of needlessly
125 // adding and removing QScreens.
126 QCFType<CFUUIDRef> uuid = CGDisplayCreateUUIDFromDisplayID(displayId);
127 Q_ASSERT(uuid);
128
129 if (QCocoaScreen *existingScreen = QCocoaScreen::get(uuid)) {
130 existingScreen->update(displayId);
131 qCInfo(lcQpaScreen) << "Updated" << existingScreen;
132 if (CGDisplayIsMain(displayId) && existingScreen != qGuiApp->primaryScreen()->handle()) {
133 qCInfo(lcQpaScreen) << "Primary screen changed to" << existingScreen;
134 QWindowSystemInterface::handlePrimaryScreenChanged(existingScreen);
135 }
136 } else {
137 QCocoaScreen::add(displayId);
138 }
139 }
140
141 for (QScreen *screen : QGuiApplication::screens()) {
142 QCocoaScreen *platformScreen = static_cast<QCocoaScreen*>(screen->handle());
143 if (!platformScreen->isOnline() || platformScreen->isMirroring())
144 platformScreen->remove();
145 }
146}
147
148void QCocoaScreen::add(CGDirectDisplayID displayId)
149{
150 const bool isPrimary = CGDisplayIsMain(displayId);
151 QCocoaScreen *cocoaScreen = new QCocoaScreen(displayId);
152 qCInfo(lcQpaScreen) << "Adding" << cocoaScreen
153 << (isPrimary ? "as new primary screen" : "");
154 QWindowSystemInterface::handleScreenAdded(cocoaScreen, isPrimary);
155}
156
157QCocoaScreen::QCocoaScreen(CGDirectDisplayID displayId)
158 : QPlatformScreen(), m_displayId(displayId)
159{
160 update(m_displayId);
161 m_cursor = new QCocoaCursor;
162}
163
164void QCocoaScreen::cleanupScreens()
165{
166 // Remove screens in reverse order to avoid crash in case of multiple screens
167 for (QScreen *screen : backwards(QGuiApplication::screens()))
168 static_cast<QCocoaScreen*>(screen->handle())->remove();
169
170 Q_ASSERT(s_displayReconfigurationCallBack);
171 CGDisplayRemoveReconfigurationCallback(s_displayReconfigurationCallBack, nullptr);
172 s_displayReconfigurationCallBack = nullptr;
173
174 s_screenParameterObserver.remove();
175}
176
177void QCocoaScreen::remove()
178{
179 // This may result in the application responding to QGuiApplication::screenRemoved
180 // by moving the window to another screen, either by setGeometry, or by setScreen.
181 // If the window isn't moved by the application, Qt will as a fallback move it to
182 // the primary screen via setScreen. Due to the way setScreen works, this won't
183 // actually recreate the window on the new screen, it will just assign the new
184 // QScreen to the window. The associated NSWindow will have an NSScreen determined
185 // by AppKit. AppKit will then move the window to another screen by changing the
186 // geometry, and we will get a callback in QCocoaWindow::windowDidMove and then
187 // QCocoaWindow::windowDidChangeScreen. At that point the window will appear to have
188 // already changed its screen, but that's only true if comparing the Qt screens,
189 // not when comparing the NSScreens.
190 qCInfo(lcQpaScreen) << "Removing " << this;
191 QWindowSystemInterface::handleScreenRemoved(this);
192}
193
195{
196 Q_ASSERT_X(!screen(), "QCocoaScreen", "QScreen should be deleted first");
197
198 delete m_cursor;
199
200 CVDisplayLinkRelease(m_displayLink);
201 if (m_displayLinkSource)
202 dispatch_release(m_displayLinkSource);
203}
204
205void QCocoaScreen::update(CGDirectDisplayID displayId)
206{
207 if (displayId != m_displayId) {
208 qCDebug(lcQpaScreen) << "Reconnecting" << this << "as display" << displayId;
209 m_displayId = displayId;
210 }
211
212 Q_ASSERT(isOnline());
213
214 // Some properties are only available via NSScreen
215 NSScreen *nsScreen = nativeScreen();
216 if (!nsScreen) {
217 qCDebug(lcQpaScreen) << "Corresponding NSScreen not yet available. Deferring update";
218 return;
219 }
220
221 const QRect previousGeometry = m_geometry;
222 const QRect previousAvailableGeometry = m_availableGeometry;
223 const qreal previousRefreshRate = m_refreshRate;
224 const double previousRotation = m_rotation;
225
226 // The reference screen for the geometry is always the primary screen
227 QRectF primaryScreenGeometry = QRectF::fromCGRect(CGDisplayBounds(CGMainDisplayID()));
228 m_geometry = qt_mac_flip(QRectF::fromCGRect(nsScreen.frame), primaryScreenGeometry).toRect();
229 m_availableGeometry = qt_mac_flip(QRectF::fromCGRect(nsScreen.visibleFrame), primaryScreenGeometry).toRect();
230
231 m_devicePixelRatio = nsScreen.backingScaleFactor;
232
233 m_format = QImage::Format_RGB32;
234 m_depth = NSBitsPerPixelFromDepth(nsScreen.depth);
235 m_colorSpace = QColorSpace::fromIccProfile(QByteArray::fromNSData(nsScreen.colorSpace.ICCProfileData));
236 if (!m_colorSpace.isValid()) {
237 qCWarning(lcQpaScreen) << "Failed to parse ICC profile for" << nsScreen.colorSpace
238 << "with ICC data" << nsScreen.colorSpace.ICCProfileData
239 << "- Falling back to sRGB";
240 m_colorSpace = QColorSpace::SRgb;
241 }
242
243 CGSize size = CGDisplayScreenSize(m_displayId);
244 m_physicalSize = QSizeF(size.width, size.height);
245
246 QCFType<CGDisplayModeRef> displayMode = CGDisplayCopyDisplayMode(m_displayId);
247 float refresh = CGDisplayModeGetRefreshRate(displayMode);
248 m_refreshRate = refresh > 0 ? refresh : 60.0;
249 m_rotation = CGDisplayRotation(displayId);
250 m_name = QString::fromNSString(nsScreen.localizedName);
251
252 const bool didChangeGeometry = m_geometry != previousGeometry || m_availableGeometry != previousAvailableGeometry;
253
254 if (m_rotation != previousRotation)
255 QWindowSystemInterface::handleScreenOrientationChange(screen(), orientation());
256
257 if (didChangeGeometry)
258 QWindowSystemInterface::handleScreenGeometryChange(screen(), geometry(), availableGeometry());
259 if (m_refreshRate != previousRefreshRate)
260 QWindowSystemInterface::handleScreenRefreshRateChange(screen(), m_refreshRate);
261}
262
263// ----------------------- Display link -----------------------
264
265Q_LOGGING_CATEGORY(lcQpaScreenUpdates, "qt.qpa.screen.updates", QtCriticalMsg);
266
268{
269 Q_ASSERT(m_displayId);
270
271 if (!isOnline()) {
272 qCDebug(lcQpaScreenUpdates) << this << "is not online. Ignoring update request";
273 return false;
274 }
275
276 // Track how many update requests we have queued, so that we
277 // know whether the display-link thread should try to deliver
278 // update requests, or if it can bail out early.
279 ++m_pendingUpdateRequests;
280
281 if (!m_displayLink) {
282 qCDebug(lcQpaScreenUpdates) << "Creating display link for" << this;
283 if (CVDisplayLinkCreateWithCGDisplay(m_displayId, &m_displayLink) != kCVReturnSuccess) {
284 qCWarning(lcQpaScreenUpdates) << "Failed to create display link for" << this;
285 return false;
286 }
287 if (auto displayId = CVDisplayLinkGetCurrentCGDisplay(m_displayLink); displayId != m_displayId) {
288 qCWarning(lcQpaScreenUpdates) << "Unexpected display" << displayId << "for display link";
289 CVDisplayLinkRelease(m_displayLink);
290 m_displayLink = nullptr;
291 return false;
292 }
293 CVDisplayLinkSetOutputCallback(m_displayLink, [](CVDisplayLinkRef, const CVTimeStamp*,
294 const CVTimeStamp*, CVOptionFlags, CVOptionFlags*, void* displayLinkContext) -> int {
295 // FIXME: It would be nice if update requests would include timing info
296 static_cast<QCocoaScreen*>(displayLinkContext)->deliverUpdateRequests();
297 return kCVReturnSuccess;
298 }, this);
299
300 // During live window resizing -[NSWindow _resizeWithEvent:] will spin a local event loop
301 // in event-tracking mode, dequeuing only the mouse drag events needed to update the window's
302 // frame. It will repeatedly spin this loop until no longer receiving any mouse drag events,
303 // and will then update the frame (effectively coalescing/compressing the events). Unfortunately
304 // the events are pulled out using -[NSApplication nextEventMatchingEventMask:untilDate:inMode:dequeue:]
305 // which internally uses CFRunLoopRunSpecific, so the event loop will also process GCD queues and other
306 // runloop sources that have been added to the tracking mode. This includes the GCD display-link
307 // source that we use to marshal the display-link callback over to the main thread. If the
308 // subsequent delivery of the update-request on the main thread stalls due to inefficient
309 // user code, the NSEventThread will have had time to deliver additional mouse drag events,
310 // and the logic in -[NSWindow _resizeWithEvent:] will keep on compressing events and never
311 // get to the point of actually updating the window frame, making it seem like the window
312 // is stuck in its original size. Only when the user stops moving their mouse, and the event
313 // queue is completely drained of drag events, will the window frame be updated.
314
315 // By keeping an event tap listening for drag events, registered as a version 1 runloop source,
316 // we prevent the GCD source from being prioritized, giving the resize logic enough time
317 // to finish coalescing the events. This is incidental, but conveniently gives us the behavior
318 // we are looking for, interleaving display-link updates and resize events.
319 static CFMachPortRef eventTap = []() {
320 CFMachPortRef eventTap = CGEventTapCreateForPid(getpid(), kCGTailAppendEventTap,
321 kCGEventTapOptionListenOnly, NSEventMaskLeftMouseDragged,
322 [](CGEventTapProxy, CGEventType type, CGEventRef event, void *) -> CGEventRef {
323 if (type == kCGEventTapDisabledByTimeout)
324 qCWarning(lcQpaScreenUpdates) << "Event tap disabled due to timeout!";
325 return event; // Listen only tap, so what we return doesn't really matter
326 }, nullptr);
327 CGEventTapEnable(eventTap, false); // Event taps are normally enabled when created
328 static CFRunLoopSourceRef runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0);
329 CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, kCFRunLoopCommonModes);
330
331 NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
332 [center addObserverForName:NSWindowWillStartLiveResizeNotification object:nil queue:nil
333 usingBlock:^(NSNotification *notification) {
334 qCDebug(lcQpaScreenUpdates) << "Live resize of" << notification.object
335 << "started. Enabling event tap";
336 CGEventTapEnable(eventTap, true);
337 }];
338 [center addObserverForName:NSWindowDidEndLiveResizeNotification object:nil queue:nil
339 usingBlock:^(NSNotification *notification) {
340 qCDebug(lcQpaScreenUpdates) << "Live resize of" << notification.object
341 << "ended. Disabling event tap";
342 CGEventTapEnable(eventTap, false);
343 }];
344 return eventTap;
345 }();
346 Q_UNUSED(eventTap);
347 }
348
349 if (!CVDisplayLinkIsRunning(m_displayLink)) {
350 qCDebug(lcQpaScreenUpdates) << "Starting display link for" << this;
351 CVDisplayLinkStart(m_displayLink);
352 }
353
354 return true;
355}
356
357// Helper to allow building up debug output in multiple steps
359{
360 DeferredDebugHelper(const QLoggingCategory &cat) {
361 if (cat.isDebugEnabled())
362 debug = new QDebug(QMessageLogger().debug(cat).nospace());
363 }
367 void flushOutput() {
368 if (debug) {
369 delete debug;
370 debug = nullptr;
371 }
372 }
373 QDebug *debug = nullptr;
374};
375
376#define qDeferredDebug(helper) if (Q_UNLIKELY(helper.debug)) *helper.debug
377
379{
380 if (!isOnline())
381 return;
382
383 QMacAutoReleasePool pool;
384
385 // The CVDisplayLink callback is a notification that it's a good time to produce a new frame.
386 // Since the callback is delivered on a separate thread we have to marshal it over to the
387 // main thread, as Qt requires update requests to be delivered there. This needs to happen
388 // asynchronously, as otherwise we may end up deadlocking if the main thread calls back
389 // into any of the CVDisplayLink APIs.
390 if (!NSThread.isMainThread) {
391 // We're explicitly not using the data of the GCD source to track the pending updates,
392 // as the data isn't reset to 0 until after the event handler, and also doesn't update
393 // during the event handler, both of which we need to track late frames.
394 const int pendingUpdates = ++m_pendingDisplayLinkUpdates;
395
396 const int pendingUpdateRequests = m_pendingUpdateRequests;
397
398 DeferredDebugHelper screenUpdates(lcQpaScreenUpdates());
399 qDeferredDebug(screenUpdates) << "display link callback for screen " << m_displayId
400 << " with " << pendingUpdateRequests << " pending update requests";
401
402 if (const int framesAheadOfDelivery = pendingUpdates - 1) {
403 // If we have more than one update pending it means that a previous display link callback
404 // has not been fully processed on the main thread, either because GCD hasn't delivered
405 // it on the main thread yet, because the processing of the update request is taking
406 // too long, or because the update request was deferred due to window live resizing.
407 qDeferredDebug(screenUpdates) << ", " << framesAheadOfDelivery << " frame(s) ahead";
408 }
409
410 if (!pendingUpdateRequests) {
411 // There's a cost to stopping and starting the display link thread,
412 // so once started we always keep it running, to avoid missing frames.
413 // In the case where we don't have any pending update requests we don't
414 // need to signal the main thread.
415 qDeferredDebug(screenUpdates) << "; skipping signaling dispatch source";
416 m_pendingDisplayLinkUpdates = 0;
417 return;
418 }
419
420 qDeferredDebug(screenUpdates) << "; signaling dispatch source";
421
422 if (!m_displayLinkSource) {
423 m_displayLinkSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue());
424 dispatch_source_set_event_handler(m_displayLinkSource, ^{
425 deliverUpdateRequests();
426 });
427 dispatch_resume(m_displayLinkSource);
428 }
429
430 dispatch_source_merge_data(m_displayLinkSource, 1);
431
432 } else {
433 DeferredDebugHelper screenUpdates(lcQpaScreenUpdates());
434 qDeferredDebug(screenUpdates) << "gcd event handler on main thread";
435
436 const int pendingUpdates = m_pendingDisplayLinkUpdates;
437 if (pendingUpdates > 1)
438 qDeferredDebug(screenUpdates) << ", " << (pendingUpdates - 1) << " frame(s) behind display link";
439
440 screenUpdates.flushOutput();
441
442 int pendingUpdateRequests = 0;
443
444 auto windows = QGuiApplication::allWindows();
445 for (int i = 0; i < windows.size(); ++i) {
446 QWindow *window = windows.at(i);
447 if (window->screen() != screen())
448 continue;
449
450 QPointer<QCocoaWindow> platformWindow = static_cast<QCocoaWindow*>(window->handle());
451 if (!platformWindow)
452 continue;
453
454 if (!platformWindow->hasPendingUpdateRequest())
455 continue;
456
457 // Skip windows that are not doing update requests via display link
458 if (!platformWindow->updatesWithDisplayLink())
459 continue;
460
461 platformWindow->deliverUpdateRequest();
462
463 // platform window can be destroyed in deliverUpdateRequest()
464 if (!platformWindow)
465 continue;
466
467 // The update request delivery could result in another request
468 // from the window, or the platform window could decide to not
469 // deliver the request at this time.
470 if (platformWindow->hasPendingUpdateRequest())
471 ++pendingUpdateRequests;
472 }
473
474 m_pendingUpdateRequests = pendingUpdateRequests;
475
476 if (const int missedUpdates = m_pendingDisplayLinkUpdates.fetchAndStoreRelaxed(0) - pendingUpdates) {
477 qCWarning(lcQpaScreenUpdates) << "main thread missed" << missedUpdates
478 << "update(s) from display link during update request delivery";
479 }
480 }
481}
482
483void QCocoaScreen::maybeStopDisplayLink()
484{
485 if (!CVDisplayLinkIsRunning(m_displayLink))
486 return;
487
488 const auto windows = QGuiApplication::allWindows();
489 for (auto *window : windows) {
490 if (window->screen() != screen())
491 continue;
492
493 QPointer<QCocoaWindow> platformWindow = static_cast<QCocoaWindow*>(window->handle());
494 if (!platformWindow)
495 continue;
496
497 if (window->isExposed())
498 return;
499
500 if (platformWindow->hasPendingUpdateRequest())
501 return;
502 }
503
504 qCDebug(lcQpaScreenUpdates) << "Stopping display link for" << this;
505 CVDisplayLinkStop(m_displayLink);
506}
507
508
509// -----------------------------------------------------------
510
511void QCocoaScreen::updateHdrWindows()
512{
513 if (@available(macOS 14, *)) {
514 for (auto *window : QGuiApplication::allWindows()) {
515 auto *platformWindow = static_cast<QCocoaWindow*>(window->handle());
516 if (!platformWindow)
517 continue;
518
519 NSView *view = platformWindow->view();
520
521 if (!view.layer.wantsExtendedDynamicRangeContent)
522 continue;
523
524 [view setNeedsDisplay:YES];
525 }
526 }
527}
528
529// -----------------------------------------------------------
530
532{
533 QPlatformScreen::SubpixelAntialiasingType type = QPlatformScreen::subpixelAntialiasingTypeHint();
534 if (type == QPlatformScreen::Subpixel_None) {
535 // Every OSX machine has RGB pixels unless a peculiar or rotated non-Apple screen is attached
536 type = QPlatformScreen::Subpixel_RGB;
537 }
538 return type;
539}
540
542{
543 if (m_rotation == 0)
544 return Qt::LandscapeOrientation;
545 if (m_rotation == 90)
546 return Qt::PortraitOrientation;
547 if (m_rotation == 180)
548 return Qt::InvertedLandscapeOrientation;
549 if (m_rotation == 270)
550 return Qt::InvertedPortraitOrientation;
551 return QPlatformScreen::orientation();
552}
553
554QWindow *QCocoaScreen::topLevelAt(const QPoint &point) const
555{
556 __block QWindow *window = nullptr;
557 [NSApp enumerateWindowsWithOptions:NSWindowListOrderedFrontToBack
558 usingBlock:^(NSWindow *nsWindow, BOOL *stop) {
559 if (!nsWindow)
560 return;
561
562 // Continue the search if the window does not belong to Qt
563 if (![nsWindow conformsToProtocol:@protocol(QNSWindowProtocol)])
564 return;
565
566 QCocoaWindow *cocoaWindow = qnsview_cast(nsWindow.contentView).platformWindow;
567 if (!cocoaWindow)
568 return;
569
570 QWindow *w = cocoaWindow->window();
571 if (!w->isVisible())
572 return;
573
574 auto nativeGeometry = QHighDpi::toNativePixels(w->geometry(), w);
575 if (!nativeGeometry.contains(point))
576 return;
577
578 QRegion mask = QHighDpi::toNativeLocalPosition(w->mask(), w);
579 if (!mask.isEmpty() && !mask.contains(point - nativeGeometry.topLeft()))
580 return;
581
582 window = w;
583
584 // Continue the search if the window is not a top-level window
585 if (!window->isTopLevel())
586 return;
587
588 *stop = true;
589 }
590 ];
591
592 return window;
593}
594
595/*!
596 \internal
597
598 Coordinates are in screen coordinates if \a view is 0, otherwise they are in view
599 coordinates.
600*/
601QPixmap QCocoaScreen::grabWindow(WId view, int x, int y, int width, int height) const
602{
603 /*
604 Grab the grabRect section of the specified display into a pixmap that has
605 sRGB color spec. Once Qt supports a fully color-managed flow and conversions
606 that don't lose the colorspec information, we would want the image to maintain
607 the color spec of the display from which it was grabbed. Ultimately, rendering
608 the returned pixmap on the same display from which it was grabbed should produce
609 identical visual results.
610 */
611 auto grabFromDisplay = [](CGDirectDisplayID displayId, const QRect &grabRect) -> QPixmap {
612 QCFType<CGImageRef> image = CGDisplayCreateImageForRect(displayId, grabRect.toCGRect());
613 const QCFType<CGColorSpaceRef> sRGBcolorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
614 if (CGImageGetColorSpace(image) != sRGBcolorSpace) {
615 qCDebug(lcQpaScreen) << "applying color correction for display" << displayId;
616 image = CGImageCreateCopyWithColorSpace(image, sRGBcolorSpace);
617 }
618 QPixmap pixmap = QPixmap::fromImage(qt_mac_toQImage(image));
619 pixmap.setDevicePixelRatio(nativeScreenForDisplayId(displayId).backingScaleFactor);
620 return pixmap;
621 };
622
623 QRect grabRect = QRect(x, y, width, height);
624 qCDebug(lcQpaScreen) << "input grab rect" << grabRect;
625
626 if (!view) {
627 // coordinates are relative to the screen
628 if (!grabRect.isValid()) // entire screen
629 grabRect = QRect(QPoint(0, 0), geometry().size());
630 else
631 grabRect.translate(-geometry().topLeft());
632 return grabFromDisplay(displayId(), grabRect);
633 }
634
635 // grab the window; grab rect in window coordinates might span multiple screens
636 NSView *nsView = reinterpret_cast<NSView*>(view);
637 NSPoint windowPoint = [nsView convertPoint:NSMakePoint(0, 0) toView:nil];
638 NSRect screenRect = [nsView.window convertRectToScreen:NSMakeRect(windowPoint.x, windowPoint.y, 1, 1)];
639 QPoint position = mapFromNative(screenRect.origin).toPoint();
640 QSize size = QRectF::fromCGRect(NSRectToCGRect(nsView.bounds)).toRect().size();
641 QRect windowRect = QRect(position, size);
642 if (!grabRect.isValid())
643 grabRect = windowRect;
644 else
645 grabRect.translate(windowRect.topLeft());
646
647 // Find which displays to grab from
648 const int maxDisplays = 128;
649 CGDirectDisplayID displays[maxDisplays];
650 CGDisplayCount displayCount;
651 CGRect cgRect = grabRect.isValid() ? grabRect.toCGRect() : CGRectInfinite;
652 const CGDisplayErr err = CGGetDisplaysWithRect(cgRect, maxDisplays, displays, &displayCount);
653 if (err || displayCount == 0)
654 return QPixmap();
655
656 qCDebug(lcQpaScreen) << "final grab rect" << grabRect << "from" << displayCount << "displays";
657
658 // Grab images from each display
659 QVector<QPixmap> pixmaps;
660 QVector<QRect> destinations;
661 for (uint i = 0; i < displayCount; ++i) {
662 auto display = displays[i];
663 const QRect displayBounds = QRectF::fromCGRect(CGDisplayBounds(display)).toRect();
664 const QRect grabBounds = displayBounds.intersected(grabRect);
665 if (grabBounds.isNull()) {
666 destinations.append(QRect());
667 pixmaps.append(QPixmap());
668 continue;
669 }
670 const QRect displayLocalGrabBounds = QRect(QPoint(grabBounds.topLeft() - displayBounds.topLeft()), grabBounds.size());
671
672 qCDebug(lcQpaScreen) << "grab display" << i << "global" << grabBounds << "local" << displayLocalGrabBounds;
673 QPixmap displayPixmap = grabFromDisplay(display, displayLocalGrabBounds);
674 // Fast path for when grabbing from a single screen only
675 if (displayCount == 1)
676 return displayPixmap;
677
678 qCDebug(lcQpaScreen) << "grab sub-image size" << displayPixmap.size() << "devicePixelRatio" << displayPixmap.devicePixelRatio();
679 pixmaps.append(displayPixmap);
680 const QRect destBounds = QRect(QPoint(grabBounds.topLeft() - grabRect.topLeft()), grabBounds.size());
681 destinations.append(destBounds);
682 }
683
684 // Determine the highest dpr, which becomes the dpr for the returned pixmap.
685 qreal dpr = 1.0;
686 for (uint i = 0; i < displayCount; ++i)
687 dpr = qMax(dpr, pixmaps.at(i).devicePixelRatio());
688
689 // Allocate target pixmap and draw each screen's content
690 qCDebug(lcQpaScreen) << "Create grap pixmap" << grabRect.size() << "at devicePixelRatio" << dpr;
691 QPixmap windowPixmap(grabRect.size() * dpr);
692 windowPixmap.setDevicePixelRatio(dpr);
693 windowPixmap.fill(Qt::transparent);
694 QPainter painter(&windowPixmap);
695 for (uint i = 0; i < displayCount; ++i)
696 painter.drawPixmap(destinations.at(i), pixmaps.at(i));
697
698 return windowPixmap;
699}
700
701bool QCocoaScreen::isOnline() const
702{
703 // When a display is disconnected CGDisplayIsOnline and other CGDisplay
704 // functions that take a displayId will not return false, but will start
705 // returning -1 to signal that the displayId is invalid. Some functions
706 // will also assert or even crash in this case, so it's important that
707 // we double check if a display is online before calling other functions.
708 int isOnline = CGDisplayIsOnline(m_displayId);
709 static const int kCGDisplayIsDisconnected = 0xffffffff;
710 return isOnline != kCGDisplayIsDisconnected && isOnline;
711}
712
713/*
714 Returns true if a screen is mirroring another screen
715*/
716bool QCocoaScreen::isMirroring() const
717{
718 if (!isOnline())
719 return false;
720
721 return CGDisplayMirrorsDisplay(m_displayId);
722}
723
724/*!
725 The screen used as a reference for global window geometry
726*/
728{
729 // Note: The primary screen that Qt knows about may not match the current CGMainDisplayID()
730 // if macOS has not yet been able to inform us that the main display has changed, but we
731 // will update the primary screen accordingly once the reconfiguration callback comes in.
732 return static_cast<QCocoaScreen *>(QGuiApplication::primaryScreen()->handle());
733}
734
736{
737 QList<QPlatformScreen*> siblings;
738
739 // Screens on macOS are always part of the same virtual desktop
740 for (QScreen *screen : QGuiApplication::screens())
741 siblings << screen->handle();
742
743 return siblings;
744}
745
746QCocoaScreen *QCocoaScreen::get(NSScreen *nsScreen)
747{
748 auto displayId = nsScreen.qt_displayId;
749 auto *cocoaScreen = get(displayId);
750 if (!cocoaScreen) {
751 qCWarning(lcQpaScreen) << "Failed to map" << nsScreen
752 << "to QCocoaScreen. Doing last minute update.";
753 updateScreens();
754 cocoaScreen = get(displayId);
755 if (!cocoaScreen)
756 qCWarning(lcQpaScreen) << "Last minute update failed!";
757 }
758 return cocoaScreen;
759}
760
761QCocoaScreen *QCocoaScreen::get(CGDirectDisplayID displayId)
762{
763 for (QScreen *screen : QGuiApplication::screens()) {
764 QCocoaScreen *cocoaScreen = static_cast<QCocoaScreen*>(screen->handle());
765 if (cocoaScreen->m_displayId == displayId)
766 return cocoaScreen;
767 }
768
769 return nullptr;
770}
771
772QCocoaScreen *QCocoaScreen::get(CFUUIDRef uuid)
773{
774 for (QScreen *screen : QGuiApplication::screens()) {
775 auto *platformScreen = static_cast<QCocoaScreen*>(screen->handle());
776 if (!platformScreen->isOnline())
777 continue;
778
779 auto displayId = platformScreen->displayId();
780 QCFType<CFUUIDRef> candidateUuid(CGDisplayCreateUUIDFromDisplayID(displayId));
781 Q_ASSERT(candidateUuid);
782
783 if (candidateUuid == uuid)
784 return platformScreen;
785 }
786
787 return nullptr;
788}
789
790NSScreen *QCocoaScreen::nativeScreenForDisplayId(CGDirectDisplayID displayId)
791{
792 for (NSScreen *screen in NSScreen.screens) {
793 if (screen.qt_displayId == displayId)
794 return screen;
795 }
796 return nil;
797}
798
800{
801 if (!m_displayId)
802 return nil; // The display has been disconnected
803
804 return nativeScreenForDisplayId(m_displayId);
805}
806
807CGPoint QCocoaScreen::mapToNative(const QPointF &pos, QCocoaScreen *screen)
808{
809 Q_ASSERT(screen);
810 return qt_mac_flip(pos, screen->geometry()).toCGPoint();
811}
812
813CGRect QCocoaScreen::mapToNative(const QRectF &rect, QCocoaScreen *screen)
814{
815 Q_ASSERT(screen);
816 return qt_mac_flip(rect, screen->geometry()).toCGRect();
817}
818
819QPointF QCocoaScreen::mapFromNative(CGPoint pos, QCocoaScreen *screen)
820{
821 Q_ASSERT(screen);
822 return qt_mac_flip(QPointF::fromCGPoint(pos), screen->geometry());
823}
824
825QRectF QCocoaScreen::mapFromNative(CGRect rect, QCocoaScreen *screen)
826{
827 Q_ASSERT(screen);
828 return qt_mac_flip(QRectF::fromCGRect(rect), screen->geometry());
829}
830
831#ifndef QT_NO_DEBUG_STREAM
832QDebug operator<<(QDebug debug, const QCocoaScreen *screen)
833{
834 QDebugStateSaver saver(debug);
835 debug.nospace();
836 debug << "QCocoaScreen(" << (const void *)screen;
837 if (screen) {
838 debug << ", " << screen->name();
839 if (screen->isOnline()) {
840 if (CGDisplayIsAsleep(screen->displayId()))
841 debug << ", Sleeping";
842 if (auto mirroring = CGDisplayMirrorsDisplay(screen->displayId()))
843 debug << ", mirroring=" << mirroring;
844 } else {
845 debug << ", Offline";
846 }
847 debug << ", " << screen->geometry();
848 debug << ", dpr=" << screen->devicePixelRatio();
849 debug << ", displayId=" << screen->displayId();
850
851 if (auto nativeScreen = screen->nativeScreen())
852 debug << ", " << nativeScreen;
853 }
854 debug << ')';
855 return debug;
856}
857#endif // !QT_NO_DEBUG_STREAM
858
859QT_END_NAMESPACE
860
861#include "qcocoascreen.moc"
862
863@implementation NSScreen (QtExtras)
864
865- (CGDirectDisplayID)qt_displayId
866{
867 return [self.deviceDescription[@"NSScreenNumber"] unsignedIntValue];
868}
869
870@end
QPixmap grabWindow(WId window, int x, int y, int width, int height) const override
void deliverUpdateRequests()
bool requestUpdate()
NSScreen * nativeScreen() const override
QPlatformScreen::SubpixelAntialiasingType subpixelAntialiasingTypeHint() const override
Returns a hint about this screen's subpixel layout structure.
static QCocoaScreen * primaryScreen()
The screen used as a reference for global window geometry.
Qt::ScreenOrientation orientation() const override
Reimplement this function in subclass to return the current orientation of the screen,...
QList< QPlatformScreen * > virtualSiblings() const override
Returns a list of all the platform screens that are part of the same virtual desktop.
QWindow * topLevelAt(const QPoint &point) const override
Return the given top level window for a given position.
\inmodule QtCore\reentrant
Definition qpoint.h:231
@ ReconfiguredWithFlagsMissing
#define qDeferredDebug(helper)
Q_LOGGING_CATEGORY(lcEventDispatcher, "qt.eventdispatcher")
DeferredDebugHelper(const QLoggingCategory &cat)