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