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 if (QOperatingSystemVersion::current() < QOperatingSystemVersion::MacOSTahoe) {
233 // On macOS versions below 26 the visibleFrame wrongly includes the auto-hidden
234 // menu bar area while in native full screen, even though that area still
235 // intercepts clicks meant for the hidden menu bar. Carve it out ourselves
236 // so we don't report it as available, e.g. for popup placement.
237 if (NSApp.presentationOptions & NSApplicationPresentationFullScreen
238 && m_availableGeometry.height() == m_geometry.height()) {
239 if (auto menuBarHeight = NSApp.mainMenu.menuBarHeight; menuBarHeight > 0)
240 m_availableGeometry.setTop(m_availableGeometry.top() + qRound(menuBarHeight));
241 }
242 }
243
244 m_devicePixelRatio = nsScreen.backingScaleFactor;
245
246 m_format = QImage::Format_RGB32;
247 m_depth = NSBitsPerPixelFromDepth(nsScreen.depth);
248 m_colorSpace = QColorSpace::fromIccProfile(QByteArray::fromNSData(nsScreen.colorSpace.ICCProfileData));
249 if (!m_colorSpace.isValid()) {
250 qCWarning(lcQpaScreen) << "Failed to parse ICC profile for" << nsScreen.colorSpace
251 << "with ICC data" << nsScreen.colorSpace.ICCProfileData
252 << "- Falling back to sRGB";
253 m_colorSpace = QColorSpace::SRgb;
254 }
255
256 CGSize size = CGDisplayScreenSize(m_displayId);
257 m_physicalSize = QSizeF(size.width, size.height);
258
259 QCFType<CGDisplayModeRef> displayMode = CGDisplayCopyDisplayMode(m_displayId);
260 float refresh = CGDisplayModeGetRefreshRate(displayMode);
261 m_refreshRate = refresh > 0 ? refresh : 60.0;
262 m_rotation = CGDisplayRotation(displayId);
263 m_name = QString::fromNSString(nsScreen.localizedName);
264
265 const bool didChangeGeometry = m_geometry != previousGeometry || m_availableGeometry != previousAvailableGeometry;
266
267 if (m_rotation != previousRotation)
268 QWindowSystemInterface::handleScreenOrientationChange(screen(), orientation());
269
270 if (didChangeGeometry)
271 QWindowSystemInterface::handleScreenGeometryChange(screen(), geometry(), availableGeometry());
272 if (m_refreshRate != previousRefreshRate)
273 QWindowSystemInterface::handleScreenRefreshRateChange(screen(), m_refreshRate);
274}
275
276// ----------------------- Display link -----------------------
277
278Q_LOGGING_CATEGORY(lcQpaScreenUpdates, "qt.qpa.screen.updates", QtCriticalMsg);
279
281{
282 Q_ASSERT(m_displayId);
283
284 if (!isOnline()) {
285 qCDebug(lcQpaScreenUpdates) << this << "is not online. Ignoring update request";
286 return false;
287 }
288
289 // Track how many update requests we have queued, so that we
290 // know whether the display-link thread should try to deliver
291 // update requests, or if it can bail out early.
292 ++m_pendingUpdateRequests;
293
294 if (!m_displayLink) {
295 qCDebug(lcQpaScreenUpdates) << "Creating display link for" << this;
296 if (CVDisplayLinkCreateWithCGDisplay(m_displayId, &m_displayLink) != kCVReturnSuccess) {
297 qCWarning(lcQpaScreenUpdates) << "Failed to create display link for" << this;
298 return false;
299 }
300 if (auto displayId = CVDisplayLinkGetCurrentCGDisplay(m_displayLink); displayId != m_displayId) {
301 qCWarning(lcQpaScreenUpdates) << "Unexpected display" << displayId << "for display link";
302 CVDisplayLinkRelease(m_displayLink);
303 m_displayLink = nullptr;
304 return false;
305 }
306 CVDisplayLinkSetOutputCallback(m_displayLink, [](CVDisplayLinkRef, const CVTimeStamp*,
307 const CVTimeStamp*, CVOptionFlags, CVOptionFlags*, void* displayLinkContext) -> int {
308 // FIXME: It would be nice if update requests would include timing info
309 static_cast<QCocoaScreen*>(displayLinkContext)->deliverUpdateRequests();
310 return kCVReturnSuccess;
311 }, this);
312
313 // During live window resizing -[NSWindow _resizeWithEvent:] will spin a local event loop
314 // in event-tracking mode, dequeuing only the mouse drag events needed to update the window's
315 // frame. It will repeatedly spin this loop until no longer receiving any mouse drag events,
316 // and will then update the frame (effectively coalescing/compressing the events). Unfortunately
317 // the events are pulled out using -[NSApplication nextEventMatchingEventMask:untilDate:inMode:dequeue:]
318 // which internally uses CFRunLoopRunSpecific, so the event loop will also process GCD queues and other
319 // runloop sources that have been added to the tracking mode. This includes the GCD display-link
320 // source that we use to marshal the display-link callback over to the main thread. If the
321 // subsequent delivery of the update-request on the main thread stalls due to inefficient
322 // user code, the NSEventThread will have had time to deliver additional mouse drag events,
323 // and the logic in -[NSWindow _resizeWithEvent:] will keep on compressing events and never
324 // get to the point of actually updating the window frame, making it seem like the window
325 // is stuck in its original size. Only when the user stops moving their mouse, and the event
326 // queue is completely drained of drag events, will the window frame be updated.
327
328 // By keeping an event tap listening for drag events, registered as a version 1 runloop source,
329 // we prevent the GCD source from being prioritized, giving the resize logic enough time
330 // to finish coalescing the events. This is incidental, but conveniently gives us the behavior
331 // we are looking for, interleaving display-link updates and resize events.
332 static CFMachPortRef eventTap = []() {
333 CFMachPortRef eventTap = CGEventTapCreateForPid(getpid(), kCGTailAppendEventTap,
334 kCGEventTapOptionListenOnly, NSEventMaskLeftMouseDragged,
335 [](CGEventTapProxy, CGEventType type, CGEventRef event, void *) -> CGEventRef {
336 if (type == kCGEventTapDisabledByTimeout)
337 qCWarning(lcQpaScreenUpdates) << "Event tap disabled due to timeout!";
338 return event; // Listen only tap, so what we return doesn't really matter
339 }, nullptr);
340 CGEventTapEnable(eventTap, false); // Event taps are normally enabled when created
341 static CFRunLoopSourceRef runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0);
342 CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, kCFRunLoopCommonModes);
343
344 NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
345 [center addObserverForName:NSWindowWillStartLiveResizeNotification object:nil queue:nil
346 usingBlock:^(NSNotification *notification) {
347 qCDebug(lcQpaScreenUpdates) << "Live resize of" << notification.object
348 << "started. Enabling event tap";
349 CGEventTapEnable(eventTap, true);
350 }];
351 [center addObserverForName:NSWindowDidEndLiveResizeNotification object:nil queue:nil
352 usingBlock:^(NSNotification *notification) {
353 qCDebug(lcQpaScreenUpdates) << "Live resize of" << notification.object
354 << "ended. Disabling event tap";
355 CGEventTapEnable(eventTap, false);
356 }];
357 return eventTap;
358 }();
359 Q_UNUSED(eventTap);
360 }
361
362 if (!CVDisplayLinkIsRunning(m_displayLink)) {
363 qCDebug(lcQpaScreenUpdates) << "Starting display link for" << this;
364 CVDisplayLinkStart(m_displayLink);
365 }
366
367 return true;
368}
369
370// Helper to allow building up debug output in multiple steps
372{
373 DeferredDebugHelper(const QLoggingCategory &cat) {
374 if (cat.isDebugEnabled())
375 debug = new QDebug(QMessageLogger().debug(cat).nospace());
376 }
380 void flushOutput() {
381 if (debug) {
382 delete debug;
383 debug = nullptr;
384 }
385 }
386 QDebug *debug = nullptr;
387};
388
389#define qDeferredDebug(helper) if (Q_UNLIKELY(helper.debug)) *helper.debug
390
392{
393 if (!isOnline()) {
394 qCDebug(lcQpaScreenUpdates) << this << "is not online. Ignoring update request delivery";
395 return;
396 }
397
398 QMacAutoReleasePool pool;
399
400 // The CVDisplayLink callback is a notification that it's a good time to produce a new frame.
401 // Since the callback is delivered on a separate thread we have to marshal it over to the
402 // main thread, as Qt requires update requests to be delivered there. This needs to happen
403 // asynchronously, as otherwise we may end up deadlocking if the main thread calls back
404 // into any of the CVDisplayLink APIs.
405 if (!NSThread.isMainThread) {
406 // We're explicitly not using the data of the GCD source to track the pending updates,
407 // as the data isn't reset to 0 until after the event handler, and also doesn't update
408 // during the event handler, both of which we need to track late frames.
409 const int pendingUpdates = ++m_pendingDisplayLinkUpdates;
410
411 const int pendingUpdateRequests = m_pendingUpdateRequests;
412
413 DeferredDebugHelper screenUpdates(lcQpaScreenUpdates());
414 qDeferredDebug(screenUpdates) << "display link callback for screen " << m_displayId
415 << " with " << pendingUpdateRequests << " pending update requests";
416
417 if (const int framesAheadOfDelivery = pendingUpdates - 1) {
418 // If we have more than one update pending it means that a previous display link callback
419 // has not been fully processed on the main thread, either because GCD hasn't delivered
420 // it on the main thread yet, because the processing of the update request is taking
421 // too long, or because the update request was deferred due to window live resizing.
422 qDeferredDebug(screenUpdates) << ", " << framesAheadOfDelivery << " frame(s) ahead";
423 }
424
425 if (!pendingUpdateRequests) {
426 // There's a cost to stopping and starting the display link thread,
427 // so once started we always keep it running, to avoid missing frames.
428 // In the case where we don't have any pending update requests we don't
429 // need to signal the main thread.
430 qDeferredDebug(screenUpdates) << "; skipping signaling dispatch source";
431 m_pendingDisplayLinkUpdates = 0;
432 return;
433 }
434
435 qDeferredDebug(screenUpdates) << "; signaling dispatch source";
436
437 if (!m_displayLinkSource) {
438 m_displayLinkSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue());
439 dispatch_source_set_event_handler(m_displayLinkSource, ^{
441 });
442 dispatch_resume(m_displayLinkSource);
443 }
444
445 dispatch_source_merge_data(m_displayLinkSource, 1);
446
447 } else {
448 DeferredDebugHelper screenUpdates(lcQpaScreenUpdates());
449 qDeferredDebug(screenUpdates) << "gcd event handler on main thread";
450
451 const int pendingUpdates = m_pendingDisplayLinkUpdates;
452 if (pendingUpdates > 1)
453 qDeferredDebug(screenUpdates) << ", " << (pendingUpdates - 1) << " frame(s) behind display link";
454
455 screenUpdates.flushOutput();
456
457 int pendingUpdateRequests = 0;
458
459 auto windows = QGuiApplication::allWindows();
460 for (int i = 0; i < windows.size(); ++i) {
461 QWindow *window = windows.at(i);
462 if (window->screen() != screen())
463 continue;
464
465 QPointer<QCocoaWindow> platformWindow = static_cast<QCocoaWindow*>(window->handle());
466 if (!platformWindow)
467 continue;
468
469 if (!platformWindow->hasPendingUpdateRequest())
470 continue;
471
472 // Skip windows that are not doing update requests via display link
473 if (!platformWindow->updatesWithDisplayLink())
474 continue;
475
476 platformWindow->deliverUpdateRequest();
477
478 // platform window can be destroyed in deliverUpdateRequest()
479 if (!platformWindow)
480 continue;
481
482 // The update request delivery could result in another request
483 // from the window, or the platform window could decide to not
484 // deliver the request at this time.
485 if (platformWindow->hasPendingUpdateRequest())
486 ++pendingUpdateRequests;
487 }
488
489 m_pendingUpdateRequests = pendingUpdateRequests;
490
491 if (const int missedUpdates = m_pendingDisplayLinkUpdates.fetchAndStoreRelaxed(0) - pendingUpdates) {
492 qCWarning(lcQpaScreenUpdates) << "main thread missed" << missedUpdates
493 << "update(s) from display link during update request delivery";
494 }
495 }
496}
497
498void QCocoaScreen::maybeStopDisplayLink()
499{
500 if (!CVDisplayLinkIsRunning(m_displayLink))
501 return;
502
503 const auto windows = QGuiApplication::allWindows();
504 for (auto *window : windows) {
505 if (window->screen() != screen())
506 continue;
507
508 QPointer<QCocoaWindow> platformWindow = static_cast<QCocoaWindow*>(window->handle());
509 if (!platformWindow)
510 continue;
511
512 if (window->isExposed())
513 return;
514
515 if (platformWindow->hasPendingUpdateRequest())
516 return;
517 }
518
519 qCDebug(lcQpaScreenUpdates) << "Stopping display link for" << this;
520 CVDisplayLinkStop(m_displayLink);
521}
522
523
524// -----------------------------------------------------------
525
526void QCocoaScreen::updateHdrWindows()
527{
528 if (@available(macOS 14, *)) {
529 for (auto *window : QGuiApplication::allWindows()) {
530 auto *platformWindow = static_cast<QCocoaWindow*>(window->handle());
531 if (!platformWindow)
532 continue;
533
534 NSView *view = platformWindow->view();
535
536 if (!view.layer.wantsExtendedDynamicRangeContent)
537 continue;
538
539 [view setNeedsDisplay:YES];
540 }
541 }
542}
543
544// -----------------------------------------------------------
545
547{
548 QPlatformScreen::SubpixelAntialiasingType type = QPlatformScreen::subpixelAntialiasingTypeHint();
549 if (type == QPlatformScreen::Subpixel_None) {
550 // Every OSX machine has RGB pixels unless a peculiar or rotated non-Apple screen is attached
551 type = QPlatformScreen::Subpixel_RGB;
552 }
553 return type;
554}
555
557{
558 if (m_rotation == 0)
559 return Qt::LandscapeOrientation;
560 if (m_rotation == 90)
561 return Qt::PortraitOrientation;
562 if (m_rotation == 180)
563 return Qt::InvertedLandscapeOrientation;
564 if (m_rotation == 270)
565 return Qt::InvertedPortraitOrientation;
566 return QPlatformScreen::orientation();
567}
568
569QWindow *QCocoaScreen::topLevelAt(const QPoint &point) const
570{
571 __block QWindow *window = nullptr;
572 [NSApp enumerateWindowsWithOptions:NSWindowListOrderedFrontToBack
573 usingBlock:^(NSWindow *nsWindow, BOOL *stop) {
574 if (!nsWindow)
575 return;
576
577 // Continue the search if the window does not belong to Qt
578 if (![nsWindow conformsToProtocol:@protocol(QNSWindowProtocol)])
579 return;
580
581 QCocoaWindow *cocoaWindow = qnsview_cast(nsWindow.contentView).platformWindow;
582 if (!cocoaWindow)
583 return;
584
585 QWindow *w = cocoaWindow->window();
586 if (!w->isVisible())
587 return;
588
589 auto nativeGeometry = QHighDpi::toNativePixels(w->geometry(), w);
590 if (!nativeGeometry.contains(point))
591 return;
592
593 QRegion mask = QHighDpi::toNativeLocalPosition(w->mask(), w);
594 if (!mask.isEmpty() && !mask.contains(point - nativeGeometry.topLeft()))
595 return;
596
597 window = w;
598
599 // Continue the search if the window is not a top-level window
600 if (!window->isTopLevel())
601 return;
602
603 *stop = true;
604 }
605 ];
606
607 return window;
608}
609
610/*!
611 \internal
612
613 Coordinates are in screen coordinates if \a view is 0, otherwise they are in view
614 coordinates.
615*/
616QPixmap QCocoaScreen::grabWindow(WId view, int x, int y, int width, int height) const
617{
618 // ScreenCaptureKit brings down WindowServer on macOS 14 x86_64 VMs in CI
619 if (!QGuiApplicationPrivate::platformIntegration()->hasCapability(
620 QPlatformIntegration::ScreenWindowGrabbing)) {
621 qCWarning(lcQpaScreen) << "Ignoring grabWindow to not bring down WindowServer";
622 return {};
623 }
624
625 auto grabFromDisplay = [](CGDirectDisplayID displayId, const QRect &grabRect) -> QPixmap {
626 QMacAutoReleasePool pool;
627
628 const qreal scale = QCocoaScreen::nativeScreenForDisplayId(displayId).backingScaleFactor;
629
630 dispatch_semaphore_t sem = dispatch_semaphore_create(0);
631 auto cleanup = qScopeGuard([sem]() { dispatch_release(sem); });
632
633 auto captureShareableContent = [sem](void (^capture)(SCShareableContent *)) {
634 // Try capturing all applications first, as that's the expectation of
635 // grabWindow. This will also trigger the permission system prompt on
636 // first use, matching the behavior of CGDisplayCreateImageForRect.
637 [SCShareableContent getShareableContentWithCompletionHandler:
638 ^(SCShareableContent *content, NSError *error) {
639 if (error)
640 qCDebug(lcQpaScreen) << "Failed to capture all windows" << error;
641 if (content) {
642 capture(content);
643 return;
644 }
645 // Fall back to capturing only our app, again matching the behavior
646 // of CGDisplayCreateImageForRect when we don't have permission to
647 // capture all applications. This also includes the Dock and wallpaper.
648 [SCShareableContent getCurrentProcessShareableContentWithCompletionHandler:
649 ^(SCShareableContent *content, NSError *error) {
650 if (error)
651 qCWarning(lcQpaScreen) << "Failed to capture own windows" << error;
652 if (content)
653 capture(content);
654 else
655 dispatch_semaphore_signal(sem);
656 }
657 ];
658 }
659 ];
660 };
661
662 __block QImage image;
663 captureShareableContent(^(SCShareableContent *content) {
664 Q_ASSERT(content);
665
666 QMacAutoReleasePool pool;
667
668 SCDisplay *scDisplay = nil;
669 for (SCDisplay *d in content.displays) {
670 if (d.displayID == displayId) {
671 scDisplay = d;
672 break;
673 }
674 }
675 if (!scDisplay) {
676 qCWarning(lcQpaScreen) << "No SCDisplay for display" << displayId;
677 dispatch_semaphore_signal(sem);
678 return;
679 }
680
681 SCContentFilter *filter = [[[SCContentFilter alloc]
682 initWithDisplay:scDisplay
683 includingApplications:content.applications
684 exceptingWindows:@[]] autorelease];
685 filter.includeMenuBar = YES;
686
687 SCStreamConfiguration *config = [[SCStreamConfiguration new] autorelease];
688 config.sourceRect = grabRect.toCGRect();
689 config.width = qRound(grabRect.width() * scale);
690 config.height = qRound(grabRect.height() * scale);
691 config.showsCursor = NO;
692 config.capturesAudio = NO;
693 config.ignoreShadowsDisplay = NO;
694 config.ignoreShadowsSingleWindow = NO;
695
696 [SCScreenshotManager captureImageWithFilter:filter configuration:config
697 completionHandler:^(CGImageRef cgImage, NSError *error) {
698 if (error)
699 qCWarning(lcQpaScreen) << "Failed screen capture" << error;
700 else if (cgImage)
701 image = qt_mac_toQImage(cgImage);
702 dispatch_semaphore_signal(sem);
703 }
704 ];
705 });
706
707 // Wait for screen capture to complete
708 if (dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC)) != 0) {
709 qCWarning(lcQpaScreen) << "Screen capture timed out for display" << displayId;
710 return QPixmap();
711 }
712
713 QPixmap pixmap = QPixmap::fromImage(image);
714 pixmap.setDevicePixelRatio(scale);
715 return pixmap;
716 };
717
718 QRect grabRect = QRect(x, y, width, height);
719 qCDebug(lcQpaScreen) << "input grab rect" << grabRect;
720
721 if (view) {
722 // Window local coordinates, so turn into global
723 NSView *nsView = reinterpret_cast<NSView*>(view);
724 NSPoint windowPoint = [nsView convertPoint:NSMakePoint(0, 0) toView:nil];
725 NSRect screenRect = [nsView.window convertRectToScreen:NSMakeRect(windowPoint.x, windowPoint.y, 1, 1)];
726 QPoint position = mapFromNative(screenRect.origin).toPoint();
727 QSize size = QRectF::fromCGRect(NSRectToCGRect(nsView.bounds)).toRect().size();
728 QRect windowRect = QRect(position, size);
729 if (!grabRect.isValid())
730 grabRect = windowRect;
731 else
732 grabRect.translate(windowRect.topLeft());
733 } else {
734 // Screen global coordinates
735 if (!grabRect.isValid())
736 grabRect = geometry();
737 }
738
739 // Find which displays to grab from
740 const int maxDisplays = 128;
741 CGDirectDisplayID displays[maxDisplays];
742 CGDisplayCount displayCount;
743 CGRect cgRect = grabRect.isValid() ? grabRect.toCGRect() : CGRectInfinite;
744 const CGDisplayErr err = CGGetDisplaysWithRect(cgRect, maxDisplays, displays, &displayCount);
745 if (err || displayCount == 0)
746 return QPixmap();
747
748 qCDebug(lcQpaScreen) << "final grab rect" << grabRect << "from" << displayCount << "displays";
749
750 // Grab images from each display, accumulating the global bounding box of the
751 // captured content. The output pixmap covers exactly that bounding box, so
752 // empty virtual-desktop space inside grabRect is not padded into the result.
753 QVector<QPixmap> pixmaps;
754 QVector<QRect> globalGrabBounds;
755 QRect outputRect;
756 for (uint i = 0; i < displayCount; ++i) {
757 auto display = displays[i];
758 const QRect displayBounds = QRectF::fromCGRect(CGDisplayBounds(display)).toRect();
759 const QRect grabBounds = displayBounds.intersected(grabRect);
760 if (grabBounds.isNull()) {
761 globalGrabBounds.append(QRect());
762 pixmaps.append(QPixmap());
763 continue;
764 }
765 const QRect displayLocalGrabBounds = QRect(QPoint(grabBounds.topLeft() - displayBounds.topLeft()), grabBounds.size());
766
767 qCDebug(lcQpaScreen) << "grab display" << i << "global" << grabBounds << "local" << displayLocalGrabBounds;
768 QPixmap displayPixmap = grabFromDisplay(display, displayLocalGrabBounds);
769 // Fast path for when grabbing from a single screen only
770 if (displayCount == 1)
771 return displayPixmap;
772
773 qCDebug(lcQpaScreen) << "grab sub-image size" << displayPixmap.size() << "devicePixelRatio" << displayPixmap.devicePixelRatio();
774 pixmaps.append(displayPixmap);
775 globalGrabBounds.append(grabBounds);
776 outputRect = outputRect.united(grabBounds);
777 }
778
779 // Determine the highest dpr, which becomes the dpr for the returned pixmap.
780 qreal dpr = 1.0;
781 for (uint i = 0; i < displayCount; ++i)
782 dpr = qMax(dpr, pixmaps.at(i).devicePixelRatio());
783
784 // Allocate target pixmap and draw each screen's content
785 qCDebug(lcQpaScreen) << "Create grap pixmap" << outputRect.size() << "at devicePixelRatio" << dpr;
786 QPixmap windowPixmap(outputRect.size() * dpr);
787 windowPixmap.setDevicePixelRatio(dpr);
788 windowPixmap.fill(Qt::transparent);
789 QPainter painter(&windowPixmap);
790 for (uint i = 0; i < displayCount; ++i) {
791 const QRect grabBounds = globalGrabBounds.at(i);
792 if (grabBounds.isNull())
793 continue;
794 const QRect dest(grabBounds.topLeft() - outputRect.topLeft(), grabBounds.size());
795 painter.drawPixmap(dest, pixmaps.at(i));
796 }
797
798 return windowPixmap;
799}
800
802{
803 // When a display is disconnected CGDisplayIsOnline and other CGDisplay
804 // functions that take a displayId will not return false, but will start
805 // returning -1 to signal that the displayId is invalid. Some functions
806 // will also assert or even crash in this case, so it's important that
807 // we double check if a display is online before calling other functions.
808 int isOnline = CGDisplayIsOnline(m_displayId);
809 static const int kCGDisplayIsDisconnected = 0xffffffff;
810 return isOnline != kCGDisplayIsDisconnected && isOnline;
811}
812
813/*
814 Returns true if a screen is mirroring another screen
815*/
816bool QCocoaScreen::isMirroring() const
817{
818 if (!isOnline())
819 return false;
820
821 return CGDisplayMirrorsDisplay(m_displayId);
822}
823
824/*!
825 The screen used as a reference for global window geometry
826*/
828{
829 // Note: The primary screen that Qt knows about may not match the current CGMainDisplayID()
830 // if macOS has not yet been able to inform us that the main display has changed, but we
831 // will update the primary screen accordingly once the reconfiguration callback comes in.
832 return static_cast<QCocoaScreen *>(QGuiApplication::primaryScreen()->handle());
833}
834
836{
837 QList<QPlatformScreen*> siblings;
838
839 // Screens on macOS are always part of the same virtual desktop
840 for (QScreen *screen : QGuiApplication::screens())
841 siblings << screen->handle();
842
843 return siblings;
844}
845
846QCocoaScreen *QCocoaScreen::get(NSScreen *nsScreen)
847{
848 auto displayId = nsScreen.qt_displayId;
849 auto *cocoaScreen = get(displayId);
850 if (!cocoaScreen) {
851 qCWarning(lcQpaScreen) << "Failed to map" << nsScreen
852 << "to QCocoaScreen. Doing last minute update.";
853 updateScreens();
854 cocoaScreen = get(displayId);
855 if (!cocoaScreen)
856 qCWarning(lcQpaScreen) << "Last minute update failed!";
857 }
858 return cocoaScreen;
859}
860
861QCocoaScreen *QCocoaScreen::get(CGDirectDisplayID displayId)
862{
863 for (QScreen *screen : QGuiApplication::screens()) {
864 QCocoaScreen *cocoaScreen = static_cast<QCocoaScreen*>(screen->handle());
865 if (cocoaScreen->m_displayId == displayId)
866 return cocoaScreen;
867 }
868
869 return nullptr;
870}
871
872QCocoaScreen *QCocoaScreen::get(CFUUIDRef uuid)
873{
874 for (QScreen *screen : QGuiApplication::screens()) {
875 auto *platformScreen = static_cast<QCocoaScreen*>(screen->handle());
876 if (!platformScreen->isOnline())
877 continue;
878
879 auto displayId = platformScreen->displayId();
880 QCFType<CFUUIDRef> candidateUuid(CGDisplayCreateUUIDFromDisplayID(displayId));
881 Q_ASSERT(candidateUuid);
882
883 if (candidateUuid == uuid)
884 return platformScreen;
885 }
886
887 return nullptr;
888}
889
890NSScreen *QCocoaScreen::nativeScreenForDisplayId(CGDirectDisplayID displayId)
891{
892 for (NSScreen *screen in NSScreen.screens) {
893 if (screen.qt_displayId == displayId)
894 return screen;
895 }
896 return nil;
897}
898
900{
901 if (!m_displayId)
902 return nil; // The display has been disconnected
903
904 return nativeScreenForDisplayId(m_displayId);
905}
906
907CGPoint QCocoaScreen::mapToNative(const QPointF &pos, QCocoaScreen *screen)
908{
909 Q_ASSERT(screen);
910 return qt_mac_flip(pos, screen->geometry()).toCGPoint();
911}
912
913CGRect QCocoaScreen::mapToNative(const QRectF &rect, QCocoaScreen *screen)
914{
915 Q_ASSERT(screen);
916 return qt_mac_flip(rect, screen->geometry()).toCGRect();
917}
918
919QPointF QCocoaScreen::mapFromNative(CGPoint pos, QCocoaScreen *screen)
920{
921 Q_ASSERT(screen);
922 return qt_mac_flip(QPointF::fromCGPoint(pos), screen->geometry());
923}
924
925QRectF QCocoaScreen::mapFromNative(CGRect rect, QCocoaScreen *screen)
926{
927 Q_ASSERT(screen);
928 return qt_mac_flip(QRectF::fromCGRect(rect), screen->geometry());
929}
930
931#ifndef QT_NO_DEBUG_STREAM
932QDebug operator<<(QDebug debug, const QCocoaScreen *screen)
933{
934 QDebugStateSaver saver(debug);
935 debug.nospace();
936 debug << "QCocoaScreen(" << (const void *)screen;
937 if (screen) {
938 debug << ", " << screen->name();
939 if (screen->isOnline()) {
940 if (CGDisplayIsAsleep(screen->displayId()))
941 debug << ", Sleeping";
942 if (auto mirroring = CGDisplayMirrorsDisplay(screen->displayId()))
943 debug << ", mirroring=" << mirroring;
944 } else {
945 debug << ", Offline";
946 }
947 debug << ", " << screen->geometry();
948 debug << ", dpr=" << screen->devicePixelRatio();
949 debug << ", displayId=" << screen->displayId();
950
951 if (auto nativeScreen = screen->nativeScreen())
952 debug << ", " << nativeScreen;
953 }
954 debug << ')';
955 return debug;
956}
957#endif // !QT_NO_DEBUG_STREAM
958
959QT_END_NAMESPACE
960
961#include "qcocoascreen.moc"
962
963@implementation NSScreen (QtExtras)
964
965- (CGDirectDisplayID)qt_displayId
966{
967 return [self.deviceDescription[@"NSScreenNumber"] unsignedIntValue];
968}
969
970@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)