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
qiosviewcontroller.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#include "qiosglobal.h"
7
8#include <QtCore/qscopedvaluerollback.h>
9#include <QtCore/private/qcore_mac_p.h>
10#include <QtGui/private/qapplekeymapper_p.h>
11
12#include <QtGui/QGuiApplication>
13#include <QtGui/QWindow>
14#include <QtGui/QScreen>
15
16#include <QtGui/private/qwindow_p.h>
17#include <QtGui/private/qguiapplication_p.h>
18
20#include "qiosscreen.h"
21#include "qiosglobal.h"
22#include "qioswindow.h"
23#include "quiview.h"
24
25#include <QtCore/qpointer.h>
26
27// -------------------------------------------------------------------------
28
29@interface QIOSViewController ()
30@property (nonatomic, assign) UIWindow *window;
31@property (nonatomic, assign) QPointer<QT_PREPEND_NAMESPACE(QIOSScreen)> platformScreen;
32@property (nonatomic, assign) BOOL changingOrientation;
33@end
34
35// -------------------------------------------------------------------------
36
37@interface QIOSDesktopManagerView : UIView
38@end
39
40@implementation QIOSDesktopManagerView
41
42- (instancetype)init
43{
44 if (!(self = [super init]))
45 return nil;
46
47 if (qEnvironmentVariableIntValue("QT_IOS_DEBUG_WINDOW_MANAGEMENT")) {
48 static UIImage *gridPattern = nil;
49 static dispatch_once_t onceToken;
50 dispatch_once(&onceToken, ^{
51 CGFloat dimension = 100.f;
52
53 UIGraphicsBeginImageContextWithOptions(CGSizeMake(dimension, dimension), YES, 0.0f);
54 CGContextRef context = UIGraphicsGetCurrentContext();
55
56 CGContextTranslateCTM(context, -0.5, -0.5);
57
58 #define gridColorWithBrightness(br)
59 [UIColor colorWithHue:0.6 saturation:0.0 brightness:br alpha:1.0].CGColor
60
61 CGContextSetFillColorWithColor(context, gridColorWithBrightness(0.05));
62 CGContextFillRect(context, CGRectMake(0, 0, dimension, dimension));
63
64 CGFloat gridLines[][2] = { { 10, 0.1 }, { 20, 0.2 }, { 100, 0.3 } };
65 for (size_t l = 0; l < sizeof(gridLines) / sizeof(gridLines[0]); ++l) {
66 CGFloat step = gridLines[l][0];
67 for (int c = step; c <= dimension; c += step) {
68 CGContextMoveToPoint(context, c, 0);
69 CGContextAddLineToPoint(context, c, dimension);
70 CGContextMoveToPoint(context, 0, c);
71 CGContextAddLineToPoint(context, dimension, c);
72 }
73
74 CGFloat brightness = gridLines[l][1];
75 CGContextSetStrokeColorWithColor(context, gridColorWithBrightness(brightness));
76 CGContextStrokePath(context);
77 }
78
79 gridPattern = UIGraphicsGetImageFromCurrentImageContext();
80 UIGraphicsEndImageContext();
81
82 [gridPattern retain];
83 });
84
85 self.backgroundColor = [UIColor colorWithPatternImage:gridPattern];
86 }
87
88 return self;
89}
90
91- (void)didAddSubview:(UIView *)subview
92{
93 Q_UNUSED(subview);
94
95 // Track UIWindow via explicit property on QIOSViewController,
96 // as the window property of our own view is not valid until
97 // the window has been shown (below).
98 UIWindow *uiWindow = self.qtViewController.window;
99
100 if (uiWindow.hidden) {
101 // Show the UIWindow the first time a QWindow is mapped to the screen.
102 // For the main screen this hides the launch screen, while for external
103 // screens this disables mirroring of the main screen, so the external
104 // screen can be used for alternate content.
105 uiWindow.hidden = NO;
106 }
107}
108
109#if !defined(Q_OS_VISIONOS)
110- (void)willRemoveSubview:(UIView *)subview
111{
112 Q_UNUSED(subview);
113
114 UIWindow *uiWindow = self.window;
115 // uiWindow can be null when closing from the ios "app manager" and the app is
116 // showing a native window like UIDocumentBrowserViewController
117 if (!uiWindow)
118 return;
119
120 if (uiWindow.screen != [UIScreen mainScreen] && self.subviews.count == 1) {
121 // We're about to remove the last view of an external screen, so go back
122 // to mirror mode, but defer it until after the view has been removed,
123 // to ensure that we don't try to layout the view that's being removed.
124 dispatch_async(dispatch_get_main_queue(), ^{
125 uiWindow.hidden = YES;
126 });
127 }
128}
129#endif
130
131- (void)layoutSubviews
132{
133 if (QGuiApplication::applicationState() == Qt::ApplicationSuspended) {
134 // Despite the OpenGL ES Programming Guide telling us to avoid all
135 // use of OpenGL while in the background, iOS will perform its view
136 // snapshotting for the app switcher after the application has been
137 // backgrounded; once for each orientation. Presumably the expectation
138 // is that no rendering needs to be done to provide an alternate
139 // orientation snapshot, just relayouting of views. But in our case,
140 // or any non-stretchable content case such as a OpenGL based game,
141 // this is not true. Instead of continuing layout, which will send
142 // potentially expensive geometry changes (with isExposed false,
143 // since we're in the background), we short-circuit the snapshotting
144 // here. iOS will still use the latest rendered frame to create the
145 // application switcher thumbnail, but it will be based on the last
146 // active orientation of the application.
147 QIOSScreen *screen = self.qtViewController.platformScreen;
148 qCDebug(lcQpaWindow) << "ignoring layout of subviews while suspended,"
149 << "likely system snapshot of" << screen->screen()->primaryOrientation();
150 return;
151 }
152
153 for (int i = int(self.subviews.count) - 1; i >= 0; --i) {
154 UIView *view = static_cast<UIView *>([self.subviews objectAtIndex:i]);
155 if (![view isKindOfClass:[QUIView class]])
156 continue;
157
158 [self layoutView: static_cast<QUIView *>(view)];
159 }
160}
161
162- (void)layoutView:(QUIView *)view
163{
164 QWindow *window = view.qwindow;
165
166 // Return early if the QIOSWindow is still constructing, as we'll
167 // take care of setting the correct window state in the constructor.
168 if (!window->handle())
169 return;
170
171 // Re-apply window states to update geometry
172 if (window->windowStates() & (Qt::WindowFullScreen | Qt::WindowMaximized))
173 window->handle()->setWindowState(window->windowStates());
174}
175
176// Even if the root view controller has both wantsFullScreenLayout and
177// extendedLayoutIncludesOpaqueBars enabled, iOS will still push the root
178// view down 20 pixels (and shrink the view accordingly) when the in-call
179// statusbar is active (instead of updating the topLayoutGuide). Since
180// we treat the root view controller as our screen, we want to reflect
181// the in-call statusbar as a change in available geometry, not in screen
182// geometry. To simplify the screen geometry mapping code we reset the
183// view modifications that iOS does and take the statusbar height
184// explicitly into account in QIOSScreen::updateProperties().
185
186- (void)setFrame:(CGRect)newFrame
187{
188 Q_UNUSED(newFrame);
189 Q_ASSERT(!self.window || self.window.rootViewController.view == self);
190
191 // When presenting view controllers our view may be temporarily reparented into a UITransitionView
192 // instead of the UIWindow, and the UITransitionView may have a transform set, so we need to do a
193 // mapping even if we still expect to always be the root view-controller.
194 CGRect transformedWindowBounds = [self.superview convertRect:self.window.bounds fromView:self.window];
195 [super setFrame:transformedWindowBounds];
196}
197
198- (void)setBounds:(CGRect)newBounds
199{
200 Q_UNUSED(newBounds);
201 CGRect transformedWindowBounds = [self convertRect:self.window.bounds fromView:self.window];
202 [super setBounds:CGRectMake(0, 0, CGRectGetWidth(transformedWindowBounds), CGRectGetHeight(transformedWindowBounds))];
203}
204
205- (void)setCenter:(CGPoint)newCenter
206{
207 Q_UNUSED(newCenter);
208 [super setCenter:self.window.center];
209}
210
211- (void)didMoveToWindow
212{
213 // The initial frame computed during startup may happen before the view has
214 // a window, meaning our calculations above will be wrong. We ensure that the
215 // frame is set correctly once we have a window to base our calculations on.
216 [self setFrame:self.window.bounds];
217}
218
219@end
220
221// -------------------------------------------------------------------------
222
223@implementation QIOSViewController {
224 BOOL m_updatingProperties;
225 QMetaObject::Connection m_focusWindowChangeConnection;
226 QMetaObject::Connection m_appStateChangedConnection;
227}
228
229#ifndef Q_OS_TVOS
230@synthesize prefersStatusBarHidden;
231@synthesize preferredStatusBarUpdateAnimation;
232@synthesize preferredStatusBarStyle;
233#endif
234
235- (instancetype)initWithWindow:(UIWindow*)window
236{
237 if (self = [self init]) {
238 self.window = window;
239 self.platformScreen = nil;
240 [self updatePlatformScreen];
241
242 self.changingOrientation = NO;
243#ifndef Q_OS_TVOS
244 // Status bar may be initially hidden at startup through Info.plist
245 self.prefersStatusBarHidden = infoPlistValue(@"UIStatusBarHidden", false);
246 self.preferredStatusBarUpdateAnimation = UIStatusBarAnimationNone;
247 self.preferredStatusBarStyle = UIStatusBarStyle(infoPlistValue(@"UIStatusBarStyle", UIStatusBarStyleDefault));
248#endif
249
250 m_focusWindowChangeConnection = QObject::connect(qApp, &QGuiApplication::focusWindowChanged, [self]() {
251 [self updateStatusBarProperties];
252 });
253
254 QIOSApplicationState *applicationState = &QIOSIntegration::instance()->applicationState;
255 m_appStateChangedConnection = QObject::connect(applicationState, &QIOSApplicationState::applicationStateDidChange,
256 [self](Qt::ApplicationState oldState, Qt::ApplicationState newState) {
257 if (oldState == Qt::ApplicationSuspended && newState != Qt::ApplicationSuspended) {
258 // We may have ignored an earlier layout because the application was suspended,
259 // and we didn't want to render anything at that moment in fear of being killed
260 // due to rendering in the background, so we trigger an explicit layout when
261 // coming out of the suspended state.
262 qCDebug(lcQpaWindow) << "triggering root VC layout when coming out of suspended state";
263 [self.view setNeedsLayout];
264 }
265 }
266 );
267 }
268
269 return self;
270}
271
272- (void)dealloc
273{
274 QObject::disconnect(m_focusWindowChangeConnection);
275 QObject::disconnect(m_appStateChangedConnection);
276 [super dealloc];
277}
278
279- (void)loadView
280{
281 self.view = [[[QIOSDesktopManagerView alloc] init] autorelease];
282}
283
284- (void)viewDidLoad
285{
286 [super viewDidLoad];
287
288 Q_ASSERT(!qt_apple_isApplicationExtension());
289
290#if !defined(Q_OS_TVOS) && !defined(Q_OS_VISIONOS)
291 NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
292 [center addObserver:self selector:@selector(willChangeStatusBarFrame:)
293 name:UIApplicationWillChangeStatusBarFrameNotification
294 object:qt_apple_sharedApplication()];
295
296 [center addObserver:self selector:@selector(didChangeStatusBarOrientation:)
297 name:UIApplicationDidChangeStatusBarOrientationNotification
298 object:qt_apple_sharedApplication()];
299#endif
300
301 // Make sure any top level windows that have already been created
302 // for this screen are reparented into our desktop manager view.
303 for (auto *window : qGuiApp->topLevelWindows()) {
304 if (window->screen()->handle() != self.platformScreen)
305 continue;
306 if (auto *platformWindow = window->handle())
307 platformWindow->setParent(nullptr);
308 }
309}
310
311- (void)viewDidUnload
312{
313 [[NSNotificationCenter defaultCenter] removeObserver:self name:nil object:nil];
314 [super viewDidUnload];
315}
316
317// -------------------------------------------------------------------------
318
319- (void)updatePlatformScreen
320{
321 auto *windowScene = self.window.windowScene;
322
323 QIOSScreen *newPlatformScreen = [&]{
324 for (auto *screen : qGuiApp->screens()) {
325 auto *platformScreen = static_cast<QIOSScreen*>(screen->handle());
326#if !defined(Q_OS_VISIONOS)
327 if (platformScreen->uiScreen() == windowScene.screen)
328#endif
329 return platformScreen;
330 }
331 Q_UNREACHABLE();
332 }();
333
334 if (newPlatformScreen != self.platformScreen) {
335 QIOSScreen *oldPlatformScreen = self.platformScreen;
336 self.platformScreen = newPlatformScreen;
337
338 qCDebug(lcQpaWindow) << "View controller" << self << "moved from"
339 << oldPlatformScreen << "to" << newPlatformScreen;
340
341 QScreen *newScreen = newPlatformScreen ? newPlatformScreen->screen() : nullptr;
342
343 const bool isPrimaryScene = !qt_apple_sharedApplication().supportsMultipleScenes
344 && windowScene.session.role == UIWindowSceneSessionRoleApplication;
345
346 if (isPrimaryScene) {
347 // When we only have a single application-role scene we treat the
348 // active screen as the primary one, so that windows shown on the
349 // primary screen end up in our view controller.
350 QWindowSystemInterface::handlePrimaryScreenChanged(newPlatformScreen);
351 }
352
353 for (auto *window : qGuiApp->topLevelWindows()) {
354 // Move window to new screen if it was on the old screen,
355 // or if we're setting up the primary scene, in which case
356 // we want to adopt all existing windows to this screen.
357 if ((window->screen()->handle() == oldPlatformScreen)
358 || (isPrimaryScene && !oldPlatformScreen)) {
359 QWindowSystemInterface::handleWindowScreenChanged(window, newScreen);
360 }
361 }
362 }
363}
364
365// -------------------------------------------------------------------------
366
367- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)orientation duration:(NSTimeInterval)duration
368{
369 self.changingOrientation = YES;
370
371 [super willRotateToInterfaceOrientation:orientation duration:duration];
372}
373
374- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)orientation
375{
376 self.changingOrientation = NO;
377
378 [super didRotateFromInterfaceOrientation:orientation];
379}
380
381#if !defined(Q_OS_VISIONOS)
382- (void)willChangeStatusBarFrame:(NSNotification*)notification
383{
384 Q_UNUSED(notification);
385
386 if (self.view.window.screen != [UIScreen mainScreen])
387 return;
388
389 // Orientation changes will already result in laying out subviews, so we don't
390 // need to do anything extra for frame changes during an orientation change.
391 // Technically we can receive another actual statusbar frame update during the
392 // orientation change that we should react to, but to simplify the logic we
393 // use a simple bool variable instead of a ignoreNextFrameChange approach.
394 if (self.changingOrientation)
395 return;
396
397 // UIKit doesn't have a delegate callback for statusbar changes that's run inside the
398 // animation block, like UIViewController's willAnimateRotationToInterfaceOrientation,
399 // nor does it expose a constant for the duration and easing of the animation. However,
400 // though poking at the various UIStatusBar methods, we can observe that the animation
401 // uses the default easing curve, and runs with a duration of 0.35 seconds.
402 static qreal kUIStatusBarAnimationDuration = 0.35;
403
404 [UIView animateWithDuration:kUIStatusBarAnimationDuration animations:^{
405 [self.view setNeedsLayout];
406 [self.view layoutIfNeeded];
407 }];
408}
409
410- (void)didChangeStatusBarOrientation:(NSNotification *)notification
411{
412 Q_UNUSED(notification);
413
414 if (self.view.window.screen != [UIScreen mainScreen])
415 return;
416
417 // If the statusbar changes orientation due to auto-rotation we don't care,
418 // there will be re-layout anyways. Only if the statusbar changes due to
419 // reportContentOrientation, we need to update the window layout.
420 if (!self.changingOrientation)
421 [self.view setNeedsLayout];
422
423 // But we always need to update the screen's orientation
424 if (self.platformScreen)
425 self.platformScreen->updateProperties();
426}
427#endif
428
429- (void)viewWillLayoutSubviews
430{
431 if (!QCoreApplication::instance())
432 return;
433
434 // Make sure the screen properties are up to date before layout.
435 // We need this here, even if we also react to status bar orientation
436 // changes, as only the main screen on iOS has a statusbar.
437 if (self.platformScreen)
438 self.platformScreen->updateProperties();
439}
440
441// -------------------------------------------------------------------------
442
443- (void)updateStatusBarProperties
444{
445 if (!isQtApplication())
446 return;
447
448 if (!self.platformScreen || !self.platformScreen->screen())
449 return;
450
451#if !defined(Q_OS_VISIONOS)
452 // For now we only care about the main screen, as both the statusbar
453 // visibility and orientation is only appropriate for the main screen.
454 if (self.platformScreen->uiScreen() != [UIScreen mainScreen])
455 return;
456#endif
457
458 // Prevent recursion caused by updating the status bar appearance (position
459 // or visibility), which in turn may cause a layout of our subviews, and
460 // a reset of window-states, which themselves affect the view controller
461 // properties such as the statusbar visibility.
462 if (m_updatingProperties)
463 return;
464
465 QScopedValueRollback<BOOL> updateRollback(m_updatingProperties, YES);
466
467 QWindow *focusWindow = QGuiApplication::focusWindow();
468
469 // If we don't have a focus window we leave the statusbar
470 // as is, so that the user can activate a new window with
471 // the same window state without the status bar jumping
472 // back and forth.
473 if (!focusWindow)
474 return;
475
476 // We only care about changes to focusWindow that involves our screen
477 if (!focusWindow->screen() || focusWindow->screen()->handle() != self.platformScreen)
478 return;
479
480 // All decisions are based on the top level window
481 focusWindow = qt_window_private(focusWindow)->topLevelWindow();
482
483#if !defined(Q_OS_TVOS) && !defined(Q_OS_VISIONOS)
484
485 // -------------- Status bar style and visbility ---------------
486
487 UIStatusBarStyle oldStatusBarStyle = self.preferredStatusBarStyle;
488 if (focusWindow->flags() & Qt::ExpandedClientAreaHint)
489 self.preferredStatusBarStyle = UIStatusBarStyleDefault;
490 else
491 self.preferredStatusBarStyle = UIStatusBarStyleLightContent;
492
493 if (self.preferredStatusBarStyle != oldStatusBarStyle)
494 [self setNeedsStatusBarAppearanceUpdate];
495
496 bool currentStatusBarVisibility = self.prefersStatusBarHidden;
497 self.prefersStatusBarHidden = focusWindow->windowState() == Qt::WindowFullScreen;
498
499 if (self.prefersStatusBarHidden != currentStatusBarVisibility) {
500 [self setNeedsStatusBarAppearanceUpdate];
501 [self.view setNeedsLayout];
502 }
503#endif
504}
505
506- (NSArray*)keyCommands
507{
508 // FIXME: If we are on iOS 13.4 or later we can use UIKey instead of doing this
509 // So it should be safe to remove this entire function and handleShortcut() as
510 // a result
511 NSMutableArray<UIKeyCommand *> *keyCommands = nil;
512 QShortcutMap &shortcutMap = QGuiApplicationPrivate::instance()->shortcutMap;
513 keyCommands = [[NSMutableArray<UIKeyCommand *> alloc] init];
514 const QList<QKeySequence> keys = shortcutMap.keySequences();
515 for (const QKeySequence &seq : keys) {
516 const QString keyString = seq.toString();
517 [keyCommands addObject:[UIKeyCommand
518 keyCommandWithInput:QString(keyString[keyString.length() - 1]).toNSString()
519 modifierFlags:QAppleKeyMapper::toUIKitModifiers(seq[0].keyboardModifiers())
520 action:@selector(handleShortcut:)]];
521 }
522 return keyCommands;
523}
524
525- (void)handleShortcut:(UIKeyCommand *)keyCommand
526{
527 const QString str = QString::fromNSString([keyCommand input]);
528 Qt::KeyboardModifiers qtMods = QAppleKeyMapper::fromUIKitModifiers(keyCommand.modifierFlags);
529 QChar ch = str.isEmpty() ? QChar() : str.at(0);
530 QShortcutMap &shortcutMap = QGuiApplicationPrivate::instance()->shortcutMap;
531 QKeyEvent keyEvent(QEvent::ShortcutOverride, Qt::Key(ch.toUpper().unicode()), qtMods, str);
532 shortcutMap.tryShortcut(&keyEvent);
533}
534
535
536
537@end
#define gridColorWithBrightness(br)