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