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