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