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 qCDebug(lcQpaScreenUpdates) << this << "is not online. Ignoring update request delivery";
382 return;
383 }
384
385 QMacAutoReleasePool pool;
386
387 // The CVDisplayLink callback is a notification that it's a good time to produce a new frame.
388 // Since the callback is delivered on a separate thread we have to marshal it over to the
389 // main thread, as Qt requires update requests to be delivered there. This needs to happen
390 // asynchronously, as otherwise we may end up deadlocking if the main thread calls back
391 // into any of the CVDisplayLink APIs.
392 if (!NSThread.isMainThread) {
393 // We're explicitly not using the data of the GCD source to track the pending updates,
394 // as the data isn't reset to 0 until after the event handler, and also doesn't update
395 // during the event handler, both of which we need to track late frames.
396 const int pendingUpdates = ++m_pendingDisplayLinkUpdates;
397
398 const int pendingUpdateRequests = m_pendingUpdateRequests;
399
400 DeferredDebugHelper screenUpdates(lcQpaScreenUpdates());
401 qDeferredDebug(screenUpdates) << "display link callback for screen " << m_displayId
402 << " with " << pendingUpdateRequests << " pending update requests";
403
404 if (const int framesAheadOfDelivery = pendingUpdates - 1) {
405 // If we have more than one update pending it means that a previous display link callback
406 // has not been fully processed on the main thread, either because GCD hasn't delivered
407 // it on the main thread yet, because the processing of the update request is taking
408 // too long, or because the update request was deferred due to window live resizing.
409 qDeferredDebug(screenUpdates) << ", " << framesAheadOfDelivery << " frame(s) ahead";
410 }
411
412 if (!pendingUpdateRequests) {
413 // There's a cost to stopping and starting the display link thread,
414 // so once started we always keep it running, to avoid missing frames.
415 // In the case where we don't have any pending update requests we don't
416 // need to signal the main thread.
417 qDeferredDebug(screenUpdates) << "; skipping signaling dispatch source";
418 m_pendingDisplayLinkUpdates = 0;
419 return;
420 }
421
422 qDeferredDebug(screenUpdates) << "; signaling dispatch source";
423
424 if (!m_displayLinkSource) {
425 m_displayLinkSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue());
426 dispatch_source_set_event_handler(m_displayLinkSource, ^{
428 });
429 dispatch_resume(m_displayLinkSource);
430 }
431
432 dispatch_source_merge_data(m_displayLinkSource, 1);
433
434 } else {
435 DeferredDebugHelper screenUpdates(lcQpaScreenUpdates());
436 qDeferredDebug(screenUpdates) << "gcd event handler on main thread";
437
438 const int pendingUpdates = m_pendingDisplayLinkUpdates;
439 if (pendingUpdates > 1)
440 qDeferredDebug(screenUpdates) << ", " << (pendingUpdates - 1) << " frame(s) behind display link";
441
442 screenUpdates.flushOutput();
443
444 int pendingUpdateRequests = 0;
445
446 auto windows = QGuiApplication::allWindows();
447 for (int i = 0; i < windows.size(); ++i) {
448 QWindow *window = windows.at(i);
449 if (window->screen() != screen())
450 continue;
451
452 QPointer<QCocoaWindow> platformWindow = static_cast<QCocoaWindow*>(window->handle());
453 if (!platformWindow)
454 continue;
455
456 if (!platformWindow->hasPendingUpdateRequest())
457 continue;
458
459 // Skip windows that are not doing update requests via display link
460 if (!platformWindow->updatesWithDisplayLink())
461 continue;
462
463 platformWindow->deliverUpdateRequest();
464
465 // platform window can be destroyed in deliverUpdateRequest()
466 if (!platformWindow)
467 continue;
468
469 // The update request delivery could result in another request
470 // from the window, or the platform window could decide to not
471 // deliver the request at this time.
472 if (platformWindow->hasPendingUpdateRequest())
473 ++pendingUpdateRequests;
474 }
475
476 m_pendingUpdateRequests = pendingUpdateRequests;
477
478 if (const int missedUpdates = m_pendingDisplayLinkUpdates.fetchAndStoreRelaxed(0) - pendingUpdates) {
479 qCWarning(lcQpaScreenUpdates) << "main thread missed" << missedUpdates
480 << "update(s) from display link during update request delivery";
481 }
482 }
483}
484
485void QCocoaScreen::maybeStopDisplayLink()
486{
487 if (!CVDisplayLinkIsRunning(m_displayLink))
488 return;
489
490 const auto windows = QGuiApplication::allWindows();
491 for (auto *window : windows) {
492 if (window->screen() != screen())
493 continue;
494
495 QPointer<QCocoaWindow> platformWindow = static_cast<QCocoaWindow*>(window->handle());
496 if (!platformWindow)
497 continue;
498
499 if (window->isExposed())
500 return;
501
502 if (platformWindow->hasPendingUpdateRequest())
503 return;
504 }
505
506 qCDebug(lcQpaScreenUpdates) << "Stopping display link for" << this;
507 CVDisplayLinkStop(m_displayLink);
508}
509
510
511// -----------------------------------------------------------
512
513void QCocoaScreen::updateHdrWindows()
514{
515 if (@available(macOS 14, *)) {
516 for (auto *window : QGuiApplication::allWindows()) {
517 auto *platformWindow = static_cast<QCocoaWindow*>(window->handle());
518 if (!platformWindow)
519 continue;
520
521 NSView *view = platformWindow->view();
522
523 if (!view.layer.wantsExtendedDynamicRangeContent)
524 continue;
525
526 [view setNeedsDisplay:YES];
527 }
528 }
529}
530
531// -----------------------------------------------------------
532
534{
535 QPlatformScreen::SubpixelAntialiasingType type = QPlatformScreen::subpixelAntialiasingTypeHint();
536 if (type == QPlatformScreen::Subpixel_None) {
537 // Every OSX machine has RGB pixels unless a peculiar or rotated non-Apple screen is attached
538 type = QPlatformScreen::Subpixel_RGB;
539 }
540 return type;
541}
542
544{
545 if (m_rotation == 0)
546 return Qt::LandscapeOrientation;
547 if (m_rotation == 90)
548 return Qt::PortraitOrientation;
549 if (m_rotation == 180)
550 return Qt::InvertedLandscapeOrientation;
551 if (m_rotation == 270)
552 return Qt::InvertedPortraitOrientation;
553 return QPlatformScreen::orientation();
554}
555
556QWindow *QCocoaScreen::topLevelAt(const QPoint &point) const
557{
558 __block QWindow *window = nullptr;
559 [NSApp enumerateWindowsWithOptions:NSWindowListOrderedFrontToBack
560 usingBlock:^(NSWindow *nsWindow, BOOL *stop) {
561 if (!nsWindow)
562 return;
563
564 // Continue the search if the window does not belong to Qt
565 if (![nsWindow conformsToProtocol:@protocol(QNSWindowProtocol)])
566 return;
567
568 QCocoaWindow *cocoaWindow = qnsview_cast(nsWindow.contentView).platformWindow;
569 if (!cocoaWindow)
570 return;
571
572 QWindow *w = cocoaWindow->window();
573 if (!w->isVisible())
574 return;
575
576 auto nativeGeometry = QHighDpi::toNativePixels(w->geometry(), w);
577 if (!nativeGeometry.contains(point))
578 return;
579
580 QRegion mask = QHighDpi::toNativeLocalPosition(w->mask(), w);
581 if (!mask.isEmpty() && !mask.contains(point - nativeGeometry.topLeft()))
582 return;
583
584 window = w;
585
586 // Continue the search if the window is not a top-level window
587 if (!window->isTopLevel())
588 return;
589
590 *stop = true;
591 }
592 ];
593
594 return window;
595}
596
597/*!
598 \internal
599
600 Coordinates are in screen coordinates if \a view is 0, otherwise they are in view
601 coordinates.
602*/
603QPixmap QCocoaScreen::grabWindow(WId view, int x, int y, int width, int height) const
604{
605 /*
606 Grab the grabRect section of the specified display into a pixmap that has
607 sRGB color spec. Once Qt supports a fully color-managed flow and conversions
608 that don't lose the colorspec information, we would want the image to maintain
609 the color spec of the display from which it was grabbed. Ultimately, rendering
610 the returned pixmap on the same display from which it was grabbed should produce
611 identical visual results.
612 */
613 auto grabFromDisplay = [](CGDirectDisplayID displayId, const QRect &grabRect) -> QPixmap {
614 QCFType<CGImageRef> image = CGDisplayCreateImageForRect(displayId, grabRect.toCGRect());
615 const QCFType<CGColorSpaceRef> sRGBcolorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
616 if (CGImageGetColorSpace(image) != sRGBcolorSpace) {
617 qCDebug(lcQpaScreen) << "applying color correction for display" << displayId;
618 image = CGImageCreateCopyWithColorSpace(image, sRGBcolorSpace);
619 }
620 QPixmap pixmap = QPixmap::fromImage(qt_mac_toQImage(image));
621 pixmap.setDevicePixelRatio(nativeScreenForDisplayId(displayId).backingScaleFactor);
622 return pixmap;
623 };
624
625 QRect grabRect = QRect(x, y, width, height);
626 qCDebug(lcQpaScreen) << "input grab rect" << grabRect;
627
628 if (!view) {
629 // coordinates are relative to the screen
630 if (!grabRect.isValid()) // entire screen
631 grabRect = QRect(QPoint(0, 0), geometry().size());
632 else
633 grabRect.translate(-geometry().topLeft());
634 return grabFromDisplay(displayId(), grabRect);
635 }
636
637 // grab the window; grab rect in window coordinates might span multiple screens
638 NSView *nsView = reinterpret_cast<NSView*>(view);
639 NSPoint windowPoint = [nsView convertPoint:NSMakePoint(0, 0) toView:nil];
640 NSRect screenRect = [nsView.window convertRectToScreen:NSMakeRect(windowPoint.x, windowPoint.y, 1, 1)];
641 QPoint position = mapFromNative(screenRect.origin).toPoint();
642 QSize size = QRectF::fromCGRect(NSRectToCGRect(nsView.bounds)).toRect().size();
643 QRect windowRect = QRect(position, size);
644 if (!grabRect.isValid())
645 grabRect = windowRect;
646 else
647 grabRect.translate(windowRect.topLeft());
648
649 // Find which displays to grab from
650 const int maxDisplays = 128;
651 CGDirectDisplayID displays[maxDisplays];
652 CGDisplayCount displayCount;
653 CGRect cgRect = grabRect.isValid() ? grabRect.toCGRect() : CGRectInfinite;
654 const CGDisplayErr err = CGGetDisplaysWithRect(cgRect, maxDisplays, displays, &displayCount);
655 if (err || displayCount == 0)
656 return QPixmap();
657
658 qCDebug(lcQpaScreen) << "final grab rect" << grabRect << "from" << displayCount << "displays";
659
660 // Grab images from each display
661 QVector<QPixmap> pixmaps;
662 QVector<QRect> destinations;
663 for (uint i = 0; i < displayCount; ++i) {
664 auto display = displays[i];
665 const QRect displayBounds = QRectF::fromCGRect(CGDisplayBounds(display)).toRect();
666 const QRect grabBounds = displayBounds.intersected(grabRect);
667 if (grabBounds.isNull()) {
668 destinations.append(QRect());
669 pixmaps.append(QPixmap());
670 continue;
671 }
672 const QRect displayLocalGrabBounds = QRect(QPoint(grabBounds.topLeft() - displayBounds.topLeft()), grabBounds.size());
673
674 qCDebug(lcQpaScreen) << "grab display" << i << "global" << grabBounds << "local" << displayLocalGrabBounds;
675 QPixmap displayPixmap = grabFromDisplay(display, displayLocalGrabBounds);
676 // Fast path for when grabbing from a single screen only
677 if (displayCount == 1)
678 return displayPixmap;
679
680 qCDebug(lcQpaScreen) << "grab sub-image size" << displayPixmap.size() << "devicePixelRatio" << displayPixmap.devicePixelRatio();
681 pixmaps.append(displayPixmap);
682 const QRect destBounds = QRect(QPoint(grabBounds.topLeft() - grabRect.topLeft()), grabBounds.size());
683 destinations.append(destBounds);
684 }
685
686 // Determine the highest dpr, which becomes the dpr for the returned pixmap.
687 qreal dpr = 1.0;
688 for (uint i = 0; i < displayCount; ++i)
689 dpr = qMax(dpr, pixmaps.at(i).devicePixelRatio());
690
691 // Allocate target pixmap and draw each screen's content
692 qCDebug(lcQpaScreen) << "Create grap pixmap" << grabRect.size() << "at devicePixelRatio" << dpr;
693 QPixmap windowPixmap(grabRect.size() * dpr);
694 windowPixmap.setDevicePixelRatio(dpr);
695 windowPixmap.fill(Qt::transparent);
696 QPainter painter(&windowPixmap);
697 for (uint i = 0; i < displayCount; ++i)
698 painter.drawPixmap(destinations.at(i), pixmaps.at(i));
699
700 return windowPixmap;
701}
702
704{
705 // When a display is disconnected CGDisplayIsOnline and other CGDisplay
706 // functions that take a displayId will not return false, but will start
707 // returning -1 to signal that the displayId is invalid. Some functions
708 // will also assert or even crash in this case, so it's important that
709 // we double check if a display is online before calling other functions.
710 int isOnline = CGDisplayIsOnline(m_displayId);
711 static const int kCGDisplayIsDisconnected = 0xffffffff;
712 return isOnline != kCGDisplayIsDisconnected && isOnline;
713}
714
715/*
716 Returns true if a screen is mirroring another screen
717*/
718bool QCocoaScreen::isMirroring() const
719{
720 if (!isOnline())
721 return false;
722
723 return CGDisplayMirrorsDisplay(m_displayId);
724}
725
726/*!
727 The screen used as a reference for global window geometry
728*/
730{
731 // Note: The primary screen that Qt knows about may not match the current CGMainDisplayID()
732 // if macOS has not yet been able to inform us that the main display has changed, but we
733 // will update the primary screen accordingly once the reconfiguration callback comes in.
734 return static_cast<QCocoaScreen *>(QGuiApplication::primaryScreen()->handle());
735}
736
738{
739 QList<QPlatformScreen*> siblings;
740
741 // Screens on macOS are always part of the same virtual desktop
742 for (QScreen *screen : QGuiApplication::screens())
743 siblings << screen->handle();
744
745 return siblings;
746}
747
748QCocoaScreen *QCocoaScreen::get(NSScreen *nsScreen)
749{
750 auto displayId = nsScreen.qt_displayId;
751 auto *cocoaScreen = get(displayId);
752 if (!cocoaScreen) {
753 qCWarning(lcQpaScreen) << "Failed to map" << nsScreen
754 << "to QCocoaScreen. Doing last minute update.";
755 updateScreens();
756 cocoaScreen = get(displayId);
757 if (!cocoaScreen)
758 qCWarning(lcQpaScreen) << "Last minute update failed!";
759 }
760 return cocoaScreen;
761}
762
763QCocoaScreen *QCocoaScreen::get(CGDirectDisplayID displayId)
764{
765 for (QScreen *screen : QGuiApplication::screens()) {
766 QCocoaScreen *cocoaScreen = static_cast<QCocoaScreen*>(screen->handle());
767 if (cocoaScreen->m_displayId == displayId)
768 return cocoaScreen;
769 }
770
771 return nullptr;
772}
773
774QCocoaScreen *QCocoaScreen::get(CFUUIDRef uuid)
775{
776 for (QScreen *screen : QGuiApplication::screens()) {
777 auto *platformScreen = static_cast<QCocoaScreen*>(screen->handle());
778 if (!platformScreen->isOnline())
779 continue;
780
781 auto displayId = platformScreen->displayId();
782 QCFType<CFUUIDRef> candidateUuid(CGDisplayCreateUUIDFromDisplayID(displayId));
783 Q_ASSERT(candidateUuid);
784
785 if (candidateUuid == uuid)
786 return platformScreen;
787 }
788
789 return nullptr;
790}
791
792NSScreen *QCocoaScreen::nativeScreenForDisplayId(CGDirectDisplayID displayId)
793{
794 for (NSScreen *screen in NSScreen.screens) {
795 if (screen.qt_displayId == displayId)
796 return screen;
797 }
798 return nil;
799}
800
802{
803 if (!m_displayId)
804 return nil; // The display has been disconnected
805
806 return nativeScreenForDisplayId(m_displayId);
807}
808
809CGPoint QCocoaScreen::mapToNative(const QPointF &pos, QCocoaScreen *screen)
810{
811 Q_ASSERT(screen);
812 return qt_mac_flip(pos, screen->geometry()).toCGPoint();
813}
814
815CGRect QCocoaScreen::mapToNative(const QRectF &rect, QCocoaScreen *screen)
816{
817 Q_ASSERT(screen);
818 return qt_mac_flip(rect, screen->geometry()).toCGRect();
819}
820
821QPointF QCocoaScreen::mapFromNative(CGPoint pos, QCocoaScreen *screen)
822{
823 Q_ASSERT(screen);
824 return qt_mac_flip(QPointF::fromCGPoint(pos), screen->geometry());
825}
826
827QRectF QCocoaScreen::mapFromNative(CGRect rect, QCocoaScreen *screen)
828{
829 Q_ASSERT(screen);
830 return qt_mac_flip(QRectF::fromCGRect(rect), screen->geometry());
831}
832
833#ifndef QT_NO_DEBUG_STREAM
834QDebug operator<<(QDebug debug, const QCocoaScreen *screen)
835{
836 QDebugStateSaver saver(debug);
837 debug.nospace();
838 debug << "QCocoaScreen(" << (const void *)screen;
839 if (screen) {
840 debug << ", " << screen->name();
841 if (screen->isOnline()) {
842 if (CGDisplayIsAsleep(screen->displayId()))
843 debug << ", Sleeping";
844 if (auto mirroring = CGDisplayMirrorsDisplay(screen->displayId()))
845 debug << ", mirroring=" << mirroring;
846 } else {
847 debug << ", Offline";
848 }
849 debug << ", " << screen->geometry();
850 debug << ", dpr=" << screen->devicePixelRatio();
851 debug << ", displayId=" << screen->displayId();
852
853 if (auto nativeScreen = screen->nativeScreen())
854 debug << ", " << nativeScreen;
855 }
856 debug << ')';
857 return debug;
858}
859#endif // !QT_NO_DEBUG_STREAM
860
861QT_END_NAMESPACE
862
863#include "qcocoascreen.moc"
864
865@implementation NSScreen (QtExtras)
866
867- (CGDirectDisplayID)qt_displayId
868{
869 return [self.deviceDescription[@"NSScreenNumber"] unsignedIntValue];
870}
871
872@end
bool isOnline() const
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:232
@ ReconfiguredWithFlagsMissing
#define qDeferredDebug(helper)
Q_LOGGING_CATEGORY(lcEventDispatcher, "qt.eventdispatcher")
DeferredDebugHelper(const QLoggingCategory &cat)