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
qiosscreen.mm
Go to the documentation of this file.
1// Copyright (C) 2016 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#undef QT_NO_FOREACH // this file contains unported legacy Q_FOREACH uses
6
7#include "qiosglobal.h"
9#include "qiosscreen.h"
10#include "qioswindow.h"
11#include <qpa/qwindowsysteminterface.h>
14#include "quiview.h"
15#include "qiostheme.h"
16#include "quiwindow.h"
17
18#include <QtCore/private/qcore_mac_p.h>
19
20#include <QtGui/qpointingdevice.h>
21#include <QtGui/private/qwindow_p.h>
22#include <QtGui/private/qguiapplication_p.h>
23#include <private/qcoregraphics_p.h>
24#include <qpa/qwindowsysteminterface.h>
25
26#include <sys/sysctl.h>
27
28// -------------------------------------------------------------------------
29
30typedef void (^DisplayLinkBlock)(CADisplayLink *displayLink);
31
32@implementation UIScreen (DisplayLinkBlock)
33- (CADisplayLink*)displayLinkWithBlock:(DisplayLinkBlock)block
34{
35 return [self displayLinkWithTarget:[[block copy] autorelease]
36 selector:@selector(invokeDisplayLinkBlock:)];
37}
38@end
39
40@implementation NSObject (DisplayLinkBlock)
41- (void)invokeDisplayLinkBlock:(CADisplayLink *)sender
42{
43 DisplayLinkBlock block = static_cast<id>(self);
44 block(sender);
45}
46@end
47
48
49// -------------------------------------------------------------------------
50
51#if !defined(Q_OS_VISIONOS)
52static QIOSScreen* qtPlatformScreenFor(UIScreen *uiScreen)
53{
54 foreach (QScreen *screen, QGuiApplication::screens()) {
55 QIOSScreen *platformScreen = static_cast<QIOSScreen *>(screen->handle());
56 if (platformScreen->uiScreen() == uiScreen)
57 return platformScreen;
58 }
59
60 return 0;
61}
62
63@interface QIOSScreenTracker : NSObject
64@end
65
66@implementation QIOSScreenTracker
67
68+ (void)load
69{
70 NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
71 [center addObserver:self selector:@selector(screenConnected:)
72 name:UIScreenDidConnectNotification object:nil];
73 [center addObserver:self selector:@selector(screenDisconnected:)
74 name:UIScreenDidDisconnectNotification object:nil];
75 [center addObserver:self selector:@selector(screenModeChanged:)
76 name:UIScreenModeDidChangeNotification object:nil];
77}
78
79+ (void)screenConnected:(NSNotification*)notification
80{
81 if (!QIOSIntegration::instance())
82 return; // Will be added when QIOSIntegration is created
83
84 QWindowSystemInterface::handleScreenAdded(new QIOSScreen([notification object]));
85}
86
87+ (void)screenDisconnected:(NSNotification*)notification
88{
89 if (!QIOSIntegration::instance())
90 return;
91
92 QIOSScreen *screen = qtPlatformScreenFor([notification object]);
93 Q_ASSERT_X(screen, Q_FUNC_INFO, "Screen disconnected that we didn't know about");
94
95 QWindowSystemInterface::handleScreenRemoved(screen);
96}
97
98+ (void)screenModeChanged:(NSNotification*)notification
99{
100 if (!QIOSIntegration::instance())
101 return;
102
103 QIOSScreen *screen = qtPlatformScreenFor([notification object]);
104 Q_ASSERT_X(screen, Q_FUNC_INFO, "Screen changed that we didn't know about");
105
106 screen->updateProperties();
107}
108
109@end
110
111#endif // !defined(Q_OS_VISIONOS)
112
113// -------------------------------------------------------------------------
114
115QT_BEGIN_NAMESPACE
116
117using namespace Qt::StringLiterals;
118
119#if !defined(Q_OS_VISIONOS)
120/*!
121 Returns the model identifier of the device.
122*/
123static QString deviceModelIdentifier()
124{
125#if TARGET_OS_SIMULATOR
126 return QString::fromLocal8Bit(qgetenv("SIMULATOR_MODEL_IDENTIFIER"));
127#else
128 static const char key[] = "hw.machine";
129
130 size_t size;
131 sysctlbyname(key, NULL, &size, NULL, 0);
132
133 QVarLengthArray<char> value(size);
134 sysctlbyname(key, value.data(), &size, NULL, 0);
135
136 return QString::fromLatin1(QByteArrayView(value.constData(), qsizetype(size)));
137#endif
138}
139#endif // !defined(Q_OS_VISIONOS)
140
141
142void QIOSScreen::initializeScreens()
143{
144#if defined(Q_OS_VISIONOS)
145 // Qt requires a screen, so let's give it a dummy one
146 QWindowSystemInterface::handleScreenAdded(new QIOSScreen);
147#else
148 Q_ASSERT([UIScreen.screens containsObject:UIScreen.mainScreen]);
149 for (UIScreen *screen in UIScreen.screens)
150 QWindowSystemInterface::handleScreenAdded(new QIOSScreen(screen));
151#endif
152}
153
154#if defined(Q_OS_VISIONOS)
155QIOSScreen::QIOSScreen()
156{
157#else
158QIOSScreen::QIOSScreen(UIScreen *screen)
159 : m_uiScreen(screen)
160{
161 QString deviceIdentifier = deviceModelIdentifier();
162
163 if (screen == [UIScreen mainScreen] && !deviceIdentifier.startsWith("AppleTV")) {
164 // Based on https://en.wikipedia.org/wiki/List_of_iOS_devices#Display
165
166 // iPhone (1st gen), 3G, 3GS, and iPod Touch (1st–3rd gen) are 18-bit devices
167 static QRegularExpression lowBitDepthDevices("^(iPhone1,[12]|iPhone2,1|iPod[1-3],1)$");
168 m_depth = deviceIdentifier.contains(lowBitDepthDevices) ? 18 : 24;
169
170 static QRegularExpression iPhoneXModels("^iPhone(10,[36])$");
171 static QRegularExpression iPhonePlusModels("^iPhone(7,1|8,2|9,[24]|10,[25])$");
172 static QRegularExpression iPadMiniModels("^iPad(2,[567]|4,[4-9]|5,[12])$");
173
174 if (deviceIdentifier.contains(iPhoneXModels)) {
175 m_physicalDpi = 458;
176 } else if (deviceIdentifier.contains(iPhonePlusModels)) {
177 m_physicalDpi = 401;
178 } else if (deviceIdentifier.startsWith("iPad")) {
179 if (deviceIdentifier.contains(iPadMiniModels))
180 m_physicalDpi = 163 * devicePixelRatio();
181 else
182 m_physicalDpi = 132 * devicePixelRatio();
183 } else {
184 // All normal iPhones, and iPods
185 m_physicalDpi = 163 * devicePixelRatio();
186 }
187 } else {
188 // External display, hard to say
189 m_depth = 24;
190 m_physicalDpi = 96;
191 }
192
193 m_displayLink = [m_uiScreen displayLinkWithBlock:^(CADisplayLink *) { deliverUpdateRequests(); }];
194 m_displayLink.paused = YES; // Enabled when clients call QWindow::requestUpdate()
195 [m_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
196
197
198 // The screen brightness might affect the EDR headroom of the display,
199 // which might affect the rendering of windows that opt in to EDR.
200 m_screenBrightnessObserver = QMacNotificationObserver(m_uiScreen,
201 UIScreenBrightnessDidChangeNotification, [&]() {
202 if (@available(iOS 17, *)) {
203 for (auto *window : QPlatformScreen::windows()) {
204 auto *platformWindow = static_cast<QIOSWindow*>(window->handle());
205 if (!platformWindow)
206 continue;
207
208 UIView *view = platformWindow->view();
209
210 if (!view.layer.wantsExtendedDynamicRangeContent)
211 continue;
212
213 [view setNeedsDisplay];
214 }
215 }
216 });
217
218 // We're pausing the display link if the application moves out of the active state,
219 // so make sure to deliver to any windows that need it once the app becomes active.
220 QObject::connect(qGuiApp, &QGuiApplication::applicationStateChanged, this, [this](auto newState) {
221 if (newState == Qt::ApplicationActive) {
222 qCDebug(lcQpaApplication) << "Attempting update request delivery after becoming active";
223 deliverUpdateRequests();
224 }
225 });
226
227#endif // !defined(Q_OS_VISIONOS))
228
229 updateProperties();
230}
231
232QIOSScreen::~QIOSScreen()
233{
234 [m_displayLink invalidate];
235}
236
237QString QIOSScreen::name() const
238{
239#if defined(Q_OS_VISIONOS)
240 return {};
241#else
242 if (m_uiScreen == [UIScreen mainScreen])
243 return QString::fromNSString([UIDevice currentDevice].model) + " built-in display"_L1;
244 else
245 return "External display"_L1;
246#endif
247}
248
249void QIOSScreen::updateProperties()
250{
251 QRect previousGeometry = m_geometry;
252 QRect previousAvailableGeometry = m_availableGeometry;
253
254#if defined(Q_OS_VISIONOS)
255 // Based on what iPad app reports
256 m_geometry = QRectF::fromCGRect(rootViewForScreen(this).bounds).toRect();
257 m_depth = 24;
258#else
259 m_geometry = QRectF::fromCGRect(m_uiScreen.bounds).toRect();
260
261 if (m_geometry != previousGeometry) {
262 // We can't use the primaryOrientation of screen(), as we haven't reported the new geometry yet
263 Qt::ScreenOrientation primaryOrientation = m_geometry.width() >= m_geometry.height() ?
264 Qt::LandscapeOrientation : Qt::PortraitOrientation;
265
266 // On iPhone 6+ devices, or when display zoom is enabled, the render buffer is scaled
267 // before being output on the physical display. We have to take this into account when
268 // computing the physical size. Note that unlike the native bounds, the physical size
269 // follows the primary orientation of the screen.
270 const QRectF physicalGeometry = mapBetween(nativeOrientation(), primaryOrientation, QRectF::fromCGRect(m_uiScreen.nativeBounds).toRect());
271
272 static const qreal millimetersPerInch = 25.4;
273 m_physicalSize = physicalGeometry.size() / m_physicalDpi * millimetersPerInch;
274 }
275
276#endif // defined(Q_OS_VISIONOS)
277
278 // UIScreen does not provide a consistent accessor for the safe area margins
279 // of the screen, and on visionOS we won't even have a UIScreen, so we report
280 // the available geometry of the screen to be the same as the full geometry.
281 // Safe area margins and maximized state is handled in QIOSWindow::setWindowState.
282 m_availableGeometry = m_geometry;
283
284 // At construction time, we don't yet have an associated QScreen, but we still want
285 // to compute the properties above so they are ready for when the QScreen attaches.
286 // Also, at destruction time the QScreen has already been torn down, so notifying
287 // Qt about changes to the screen will cause asserts in the event delivery system.
288 if (!screen())
289 return;
290
291 if (screen()->orientation() != orientation())
292 QWindowSystemInterface::handleScreenOrientationChange(screen(), orientation());
293
294 // Note: The screen orientation change and the geometry changes are not atomic, so when
295 // the former is emitted, the latter has not been reported and reflected in the QScreen
296 // API yet. But conceptually it makes sense that the orientation update happens first,
297 // and the geometry updates caused by auto-rotation happen after that.
298
299 if (m_geometry != previousGeometry || m_availableGeometry != previousAvailableGeometry)
300 QWindowSystemInterface::handleScreenGeometryChange(screen(), m_geometry, m_availableGeometry);
301}
302
303void QIOSScreen::setUpdatesPaused(bool paused)
304{
305 m_displayLink.paused = paused;
306}
307
308void QIOSScreen::deliverUpdateRequests() const
309{
310 bool pauseUpdates = true;
311
312 if (QGuiApplication::applicationState() != Qt::ApplicationActive) {
313 // The applicationWillResignActive documentation describes that the app
314 // should "use this method to pause ongoing tasks, disable timers, and
315 // throttle down OpenGL ES frame rates", so we skip update request
316 // delivery if the app is not active. Once it becomes active again
317 // we re-try the update request delivery (see QIOSScreen constructor).
318 qCDebug(lcQpaApplication) << "Skipping update request delivery and pausing display link";
319 m_displayLink.paused = true;
320 return;
321 }
322
323 QList<QWindow*> windows = QGuiApplication::allWindows();
324 for (int i = 0; i < windows.size(); ++i) {
325 QWindow *window = windows.at(i);
326 if (platformScreenForWindow(window) != this)
327 continue;
328
329 QPlatformWindow *platformWindow = window->handle();
330 if (!platformWindow)
331 continue;
332
333 if (!platformWindow->hasPendingUpdateRequest())
334 continue;
335
336 platformWindow->deliverUpdateRequest();
337
338 // Another update request was triggered, keep the display link running
339 if (platformWindow->hasPendingUpdateRequest())
340 pauseUpdates = false;
341 }
342
343 // Pause the display link if there are no pending update requests
344 m_displayLink.paused = pauseUpdates;
345}
346
347QRect QIOSScreen::geometry() const
348{
349 return m_geometry;
350}
351
352QRect QIOSScreen::availableGeometry() const
353{
354 return m_availableGeometry;
355}
356
357int QIOSScreen::depth() const
358{
359 return m_depth;
360}
361
362QImage::Format QIOSScreen::format() const
363{
364 return QImage::Format_ARGB32_Premultiplied;
365}
366
367QSizeF QIOSScreen::physicalSize() const
368{
369 return m_physicalSize;
370}
371
372QDpi QIOSScreen::logicalBaseDpi() const
373{
374 return QDpi(72, 72);
375}
376
377qreal QIOSScreen::devicePixelRatio() const
378{
379#if defined(Q_OS_VISIONOS)
380 // Based on what iPad app reports, and what Apple
381 // documents to be the default scale factor on
382 // visionOS, and the minimum scale for assets.
383 return 2.0;
384#else
385 return [m_uiScreen scale];
386#endif
387}
388
389qreal QIOSScreen::refreshRate() const
390{
391#if defined(Q_OS_VISIONOS)
392 return 120.0; // Based on what iPad app reports
393#else
394 return m_uiScreen.maximumFramesPerSecond;
395#endif
396}
397
398Qt::ScreenOrientation QIOSScreen::nativeOrientation() const
399{
400#if defined(Q_OS_VISIONOS)
401 // Based on iPad app reporting native bounds 1668x2388
402 return Qt::PortraitOrientation;
403#else
404 CGRect nativeBounds =
405#if defined(Q_OS_IOS)
406 m_uiScreen.nativeBounds;
407#else
408 m_uiScreen.bounds;
409#endif
410
411 // All known iOS devices have a native orientation of portrait, but to
412 // be on the safe side we compare the width and height of the bounds.
413 return nativeBounds.size.width >= nativeBounds.size.height ?
414 Qt::LandscapeOrientation : Qt::PortraitOrientation;
415#endif
416}
417
418Qt::ScreenOrientation QIOSScreen::orientation() const
419{
420 // We don't report UIDevice.currentDevice.orientation here,
421 // as that would report the actual orientation of the device,
422 // even if the orientation of the UI was locked to a subset
423 // of the possible orientations via the app's Info.plist or
424 // via [UIViewController supportedInterfaceOrientations].
425 auto *windowScene = rootViewForScreen(this).window.windowScene;
426 auto interfaceOrientation = windowScene ?
427 windowScene.interfaceOrientation : UIInterfaceOrientationUnknown;
428
429 // FIXME: On visionOS the interface orientation is reported
430 // as portrait, which seems strange, but at least it matches
431 // what we report as the native orientation.
432
433 switch (interfaceOrientation) {
434 case UIInterfaceOrientationPortrait:
435 return Qt::PortraitOrientation;
436 case UIInterfaceOrientationPortraitUpsideDown:
437 return Qt::InvertedPortraitOrientation;
438 case UIInterfaceOrientationLandscapeLeft:
439 return Qt::LandscapeOrientation;
440 case UIInterfaceOrientationLandscapeRight:
441 return Qt::InvertedLandscapeOrientation;
442 case UIInterfaceOrientationUnknown:
443 default:
444 // Fall back to the primary orientation, but with a concrete
445 // orientation instead of Qt::PrimaryOrientation, as when we
446 // report orientation changes the primary orientation has not
447 // been updated yet, so user's can't query it in response.
448 return m_geometry.width() >= m_geometry.height() ?
449 Qt::LandscapeOrientation : Qt::PortraitOrientation;
450 }
451}
452
453QPixmap QIOSScreen::grabWindow(WId window, int x, int y, int width, int height) const
454{
455 if (window && ![reinterpret_cast<id>(window) isKindOfClass:[UIView class]])
456 return QPixmap();
457
458 UIView *view = window ? reinterpret_cast<UIView *>(window)
459 : rootViewForScreen(this);
460
461 if (width < 0)
462 width = qMax(view.bounds.size.width - x, CGFloat(0));
463 if (height < 0)
464 height = qMax(view.bounds.size.height - y, CGFloat(0));
465
466 CGRect captureRect = [view.window convertRect:CGRectMake(x, y, width, height) fromView:view];
467 captureRect = CGRectIntersection(captureRect, view.window.bounds);
468
469 QMacAutoReleasePool autoReleasePool;
470
471 UIGraphicsImageRendererFormat *format = [UIGraphicsImageRendererFormat defaultFormat];
472 format.opaque = NO;
473 format.scale = devicePixelRatio();
474 // Could be adjusted to support HDR in the future.
475 format.preferredRange = UIGraphicsImageRendererFormatRangeStandard;
476
477 UIGraphicsImageRenderer *renderer = [[[UIGraphicsImageRenderer alloc]
478 initWithSize:captureRect.size format:format]
479 autorelease];
480
481 UIImage *screenshot = [renderer imageWithActions:^(UIGraphicsImageRendererContext *rendererContext) {
482 CGContextRef context = rendererContext.CGContext;
483 CGContextTranslateCTM(context, -captureRect.origin.x, -captureRect.origin.y);
484
485 // Draws the complete view hierarchy of view.window into the given rect, which
486 // needs to be the same aspect ratio as the view.window's size. Since we've
487 // translated the graphics context, and are potentially drawing into a smaller
488 // context than the full window, the resulting image will be a subsection of the
489 // full screen.
490 //
491 // TODO: Should only be run on the UI thread in the future. At
492 // the time of writing, QScreen::grabWindow doesn't include any
493 // requirements as to which thread it can be called from.
494 [view.window drawViewHierarchyInRect:view.window.bounds afterScreenUpdates:NO];
495 }];
496
497 return QPixmap::fromImage(qt_mac_toQImage(screenshot.CGImage));
498}
499
500#if !defined(Q_OS_VISIONOS)
501UIScreen *QIOSScreen::uiScreen() const
502{
503 return m_uiScreen;
504}
505#endif
506
507QT_END_NAMESPACE
508
509#include "moc_qiosscreen.cpp"
static QString deviceModelIdentifier()
Returns the model identifier of the device.