blob: ce2aa96ca59a5a08dfac7acf9a85c9d82381ccd7 [file] [log] [blame]
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the plugins of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include "qiosglobal.h"
#import "qiosviewcontroller.h"
#include <QtCore/qscopedvaluerollback.h>
#include <QtCore/private/qcore_mac_p.h>
#include <QtGui/QGuiApplication>
#include <QtGui/QWindow>
#include <QtGui/QScreen>
#include <QtGui/private/qwindow_p.h>
#include "qiosintegration.h"
#include "qiosscreen.h"
#include "qiosglobal.h"
#include "qioswindow.h"
#include "quiview.h"
// -------------------------------------------------------------------------
@interface QIOSViewController ()
@property (nonatomic, assign) QPointer<QT_PREPEND_NAMESPACE(QIOSScreen)> platformScreen;
@property (nonatomic, assign) BOOL changingOrientation;
@end
// -------------------------------------------------------------------------
@interface QIOSDesktopManagerView : UIView
@end
@implementation QIOSDesktopManagerView
- (instancetype)init
{
if (!(self = [super init]))
return nil;
if (qEnvironmentVariableIntValue("QT_IOS_DEBUG_WINDOW_MANAGEMENT")) {
static UIImage *gridPattern = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
CGFloat dimension = 100.f;
UIGraphicsBeginImageContextWithOptions(CGSizeMake(dimension, dimension), YES, 0.0f);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextTranslateCTM(context, -0.5, -0.5);
#define gridColorWithBrightness(br) \
[UIColor colorWithHue:0.6 saturation:0.0 brightness:br alpha:1.0].CGColor
CGContextSetFillColorWithColor(context, gridColorWithBrightness(0.05));
CGContextFillRect(context, CGRectMake(0, 0, dimension, dimension));
CGFloat gridLines[][2] = { { 10, 0.1 }, { 20, 0.2 }, { 100, 0.3 } };
for (size_t l = 0; l < sizeof(gridLines) / sizeof(gridLines[0]); ++l) {
CGFloat step = gridLines[l][0];
for (int c = step; c <= dimension; c += step) {
CGContextMoveToPoint(context, c, 0);
CGContextAddLineToPoint(context, c, dimension);
CGContextMoveToPoint(context, 0, c);
CGContextAddLineToPoint(context, dimension, c);
}
CGFloat brightness = gridLines[l][1];
CGContextSetStrokeColorWithColor(context, gridColorWithBrightness(brightness));
CGContextStrokePath(context);
}
gridPattern = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
[gridPattern retain];
});
self.backgroundColor = [UIColor colorWithPatternImage:gridPattern];
}
return self;
}
- (void)didAddSubview:(UIView *)subview
{
Q_UNUSED(subview);
QT_PREPEND_NAMESPACE(QIOSScreen) *screen = self.qtViewController.platformScreen;
// The 'window' property of our view is not valid until the window
// has been shown, so we have to access it through the QIOSScreen.
UIWindow *uiWindow = screen->uiWindow();
if (uiWindow.hidden) {
// Associate UIWindow to screen and show it the first time a QWindow
// is mapped to the screen. For external screens this means disabling
// mirroring mode and presenting alternate content on the screen.
uiWindow.screen = screen->uiScreen();
uiWindow.hidden = NO;
}
}
- (void)willRemoveSubview:(UIView *)subview
{
Q_UNUSED(subview);
Q_ASSERT(self.window);
UIWindow *uiWindow = self.window;
if (uiWindow.screen != [UIScreen mainScreen] && self.subviews.count == 1) {
// Removing the last view of an external screen, go back to mirror mode
uiWindow.screen = [UIScreen mainScreen];
uiWindow.hidden = YES;
}
}
- (void)layoutSubviews
{
if (QGuiApplication::applicationState() == Qt::ApplicationSuspended) {
// Despite the OpenGL ES Programming Guide telling us to avoid all
// use of OpenGL while in the background, iOS will perform its view
// snapshotting for the app switcher after the application has been
// backgrounded; once for each orientation. Presumably the expectation
// is that no rendering needs to be done to provide an alternate
// orientation snapshot, just relayouting of views. But in our case,
// or any non-stretchable content case such as a OpenGL based game,
// this is not true. Instead of continuing layout, which will send
// potentially expensive geometry changes (with isExposed false,
// since we're in the background), we short-circuit the snapshotting
// here. iOS will still use the latest rendered frame to create the
// application switcher thumbnail, but it will be based on the last
// active orientation of the application.
QIOSScreen *screen = self.qtViewController.platformScreen;
qCDebug(lcQpaWindow) << "ignoring layout of subviews while suspended,"
<< "likely system snapshot of" << screen->screen()->primaryOrientation();
return;
}
for (int i = int(self.subviews.count) - 1; i >= 0; --i) {
UIView *view = static_cast<UIView *>([self.subviews objectAtIndex:i]);
if (![view isKindOfClass:[QUIView class]])
continue;
[self layoutView: static_cast<QUIView *>(view)];
}
}
- (void)layoutView:(QUIView *)view
{
QWindow *window = view.qwindow;
// Return early if the QIOSWindow is still constructing, as we'll
// take care of setting the correct window state in the constructor.
if (!window->handle())
return;
// Re-apply window states to update geometry
if (window->windowStates() & (Qt::WindowFullScreen | Qt::WindowMaximized))
window->handle()->setWindowState(window->windowStates());
}
// Even if the root view controller has both wantsFullScreenLayout and
// extendedLayoutIncludesOpaqueBars enabled, iOS will still push the root
// view down 20 pixels (and shrink the view accordingly) when the in-call
// statusbar is active (instead of updating the topLayoutGuide). Since
// we treat the root view controller as our screen, we want to reflect
// the in-call statusbar as a change in available geometry, not in screen
// geometry. To simplify the screen geometry mapping code we reset the
// view modifications that iOS does and take the statusbar height
// explicitly into account in QIOSScreen::updateProperties().
- (void)setFrame:(CGRect)newFrame
{
Q_UNUSED(newFrame);
Q_ASSERT(!self.window || self.window.rootViewController.view == self);
// When presenting view controllers our view may be temporarily reparented into a UITransitionView
// instead of the UIWindow, and the UITransitionView may have a transform set, so we need to do a
// mapping even if we still expect to always be the root view-controller.
CGRect transformedWindowBounds = [self.superview convertRect:self.window.bounds fromView:self.window];
[super setFrame:transformedWindowBounds];
}
- (void)setBounds:(CGRect)newBounds
{
Q_UNUSED(newBounds);
CGRect transformedWindowBounds = [self convertRect:self.window.bounds fromView:self.window];
[super setBounds:CGRectMake(0, 0, CGRectGetWidth(transformedWindowBounds), CGRectGetHeight(transformedWindowBounds))];
}
- (void)setCenter:(CGPoint)newCenter
{
Q_UNUSED(newCenter);
[super setCenter:self.window.center];
}
- (void)didMoveToWindow
{
// The initial frame computed during startup may happen before the view has
// a window, meaning our calculations above will be wrong. We ensure that the
// frame is set correctly once we have a window to base our calulations on.
[self setFrame:self.window.bounds];
}
@end
// -------------------------------------------------------------------------
@implementation QIOSViewController {
BOOL m_updatingProperties;
QMetaObject::Connection m_focusWindowChangeConnection;
}
#ifndef Q_OS_TVOS
@synthesize prefersStatusBarHidden;
@synthesize preferredStatusBarUpdateAnimation;
@synthesize preferredStatusBarStyle;
#endif
- (instancetype)initWithQIOSScreen:(QT_PREPEND_NAMESPACE(QIOSScreen) *)screen
{
if (self = [self init]) {
self.platformScreen = screen;
self.changingOrientation = NO;
#ifndef Q_OS_TVOS
self.lockedOrientation = UIInterfaceOrientationUnknown;
// Status bar may be initially hidden at startup through Info.plist
self.prefersStatusBarHidden = infoPlistValue(@"UIStatusBarHidden", false);
self.preferredStatusBarUpdateAnimation = UIStatusBarAnimationNone;
self.preferredStatusBarStyle = UIStatusBarStyle(infoPlistValue(@"UIStatusBarStyle", UIStatusBarStyleDefault));
#endif
m_focusWindowChangeConnection = QObject::connect(qApp, &QGuiApplication::focusWindowChanged, [self]() {
[self updateProperties];
});
QIOSApplicationState *applicationState = &QIOSIntegration::instance()->applicationState;
QObject::connect(applicationState, &QIOSApplicationState::applicationStateDidChange,
[self](Qt::ApplicationState oldState, Qt::ApplicationState newState) {
if (oldState == Qt::ApplicationSuspended && newState != Qt::ApplicationSuspended) {
// We may have ignored an earlier layout because the application was suspended,
// and we didn't want to render anything at that moment in fear of being killed
// due to rendering in the background, so we trigger an explicit layout when
// coming out of the suspended state.
qCDebug(lcQpaWindow) << "triggering root VC layout when coming out of suspended state";
[self.view setNeedsLayout];
}
}
);
}
return self;
}
- (void)dealloc
{
QObject::disconnect(m_focusWindowChangeConnection);
[super dealloc];
}
- (void)loadView
{
self.view = [[[QIOSDesktopManagerView alloc] init] autorelease];
}
- (void)viewDidLoad
{
[super viewDidLoad];
Q_ASSERT(!qt_apple_isApplicationExtension());
#ifndef Q_OS_TVOS
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserver:self selector:@selector(willChangeStatusBarFrame:)
name:UIApplicationWillChangeStatusBarFrameNotification
object:qt_apple_sharedApplication()];
[center addObserver:self selector:@selector(didChangeStatusBarOrientation:)
name:UIApplicationDidChangeStatusBarOrientationNotification
object:qt_apple_sharedApplication()];
#endif
}
- (void)viewDidUnload
{
[[NSNotificationCenter defaultCenter] removeObserver:self name:nil object:nil];
[super viewDidUnload];
}
// -------------------------------------------------------------------------
- (BOOL)shouldAutorotate
{
#ifndef Q_OS_TVOS
return self.platformScreen && self.platformScreen->uiScreen() == [UIScreen mainScreen] && !self.lockedOrientation;
#else
return NO;
#endif
}
- (NSUInteger)supportedInterfaceOrientations
{
// As documented by Apple in the iOS 6.0 release notes, setStatusBarOrientation:animated:
// only works if the supportedInterfaceOrientations of the view controller is 0, making
// us responsible for ensuring that the status bar orientation is consistent. We enter
// this mode when auto-rotation is disabled due to an explicit content orientation being
// set on the focus window. Note that this is counter to what the documentation for
// supportedInterfaceOrientations says, which states that the method should not return 0.
return [self shouldAutorotate] ? UIInterfaceOrientationMaskAll : 0;
}
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)orientation duration:(NSTimeInterval)duration
{
self.changingOrientation = YES;
[super willRotateToInterfaceOrientation:orientation duration:duration];
}
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)orientation
{
self.changingOrientation = NO;
[super didRotateFromInterfaceOrientation:orientation];
}
- (void)willChangeStatusBarFrame:(NSNotification*)notification
{
Q_UNUSED(notification);
if (self.view.window.screen != [UIScreen mainScreen])
return;
// Orientation changes will already result in laying out subviews, so we don't
// need to do anything extra for frame changes during an orientation change.
// Technically we can receive another actual statusbar frame update during the
// orientation change that we should react to, but to simplify the logic we
// use a simple bool variable instead of a ignoreNextFrameChange approach.
if (self.changingOrientation)
return;
// UIKit doesn't have a delegate callback for statusbar changes that's run inside the
// animation block, like UIViewController's willAnimateRotationToInterfaceOrientation,
// nor does it expose a constant for the duration and easing of the animation. However,
// though poking at the various UIStatusBar methods, we can observe that the animation
// uses the default easing curve, and runs with a duration of 0.35 seconds.
static qreal kUIStatusBarAnimationDuration = 0.35;
[UIView animateWithDuration:kUIStatusBarAnimationDuration animations:^{
[self.view setNeedsLayout];
[self.view layoutIfNeeded];
}];
}
- (void)didChangeStatusBarOrientation:(NSNotification *)notification
{
Q_UNUSED(notification);
if (self.view.window.screen != [UIScreen mainScreen])
return;
// If the statusbar changes orientation due to auto-rotation we don't care,
// there will be re-layout anyways. Only if the statusbar changes due to
// reportContentOrientation, we need to update the window layout.
if (self.changingOrientation)
return;
[self.view setNeedsLayout];
}
- (void)viewWillLayoutSubviews
{
if (!QCoreApplication::instance())
return;
if (self.platformScreen)
self.platformScreen->updateProperties();
}
// -------------------------------------------------------------------------
- (void)updateProperties
{
if (!isQtApplication())
return;
if (!self.platformScreen || !self.platformScreen->screen())
return;
// For now we only care about the main screen, as both the statusbar
// visibility and orientation is only appropriate for the main screen.
if (self.platformScreen->uiScreen() != [UIScreen mainScreen])
return;
// Prevent recursion caused by updating the status bar appearance (position
// or visibility), which in turn may cause a layout of our subviews, and
// a reset of window-states, which themselves affect the view controller
// properties such as the statusbar visibilty.
if (m_updatingProperties)
return;
QScopedValueRollback<BOOL> updateRollback(m_updatingProperties, YES);
QWindow *focusWindow = QGuiApplication::focusWindow();
// If we don't have a focus window we leave the statusbar
// as is, so that the user can activate a new window with
// the same window state without the status bar jumping
// back and forth.
if (!focusWindow)
return;
// We only care about changes to focusWindow that involves our screen
if (!focusWindow->screen() || focusWindow->screen()->handle() != self.platformScreen)
return;
// All decisions are based on the the top level window
focusWindow = qt_window_private(focusWindow)->topLevelWindow();
#ifndef Q_OS_TVOS
// -------------- Status bar style and visbility ---------------
UIStatusBarStyle oldStatusBarStyle = self.preferredStatusBarStyle;
if (focusWindow->flags() & Qt::MaximizeUsingFullscreenGeometryHint)
self.preferredStatusBarStyle = UIStatusBarStyleDefault;
else
self.preferredStatusBarStyle = UIStatusBarStyleLightContent;
if (self.preferredStatusBarStyle != oldStatusBarStyle)
[self setNeedsStatusBarAppearanceUpdate];
bool currentStatusBarVisibility = self.prefersStatusBarHidden;
self.prefersStatusBarHidden = focusWindow->windowState() == Qt::WindowFullScreen;
if (self.prefersStatusBarHidden != currentStatusBarVisibility) {
[self setNeedsStatusBarAppearanceUpdate];
[self.view setNeedsLayout];
}
// -------------- Content orientation ---------------
UIApplication *uiApplication = qt_apple_sharedApplication();
static BOOL kAnimateContentOrientationChanges = YES;
Qt::ScreenOrientation contentOrientation = focusWindow->contentOrientation();
if (contentOrientation != Qt::PrimaryOrientation) {
// An explicit content orientation has been reported for the focus window,
// so we keep the status bar in sync with content orientation. This will ensure
// that the task bar (and associated gestures) are also rotated accordingly.
if (!self.lockedOrientation) {
// We are moving from Qt::PrimaryOrientation to an explicit orientation,
// so we need to store the current statusbar orientation, as we need it
// later when mapping screen coordinates for QScreen and for returning
// to Qt::PrimaryOrientation.
self.lockedOrientation = uiApplication.statusBarOrientation;
}
[uiApplication setStatusBarOrientation:
UIInterfaceOrientation(fromQtScreenOrientation(contentOrientation))
animated:kAnimateContentOrientationChanges];
} else {
// The content orientation is set to Qt::PrimaryOrientation, meaning
// that auto-rotation should be enabled. But we may be coming out of
// a state of locked orientation, which needs some cleanup before we
// can enable auto-rotation again.
if (self.lockedOrientation) {
// First we need to restore the statusbar to what it was at the
// time of locking the orientation, otherwise iOS will be very
// confused when it starts doing auto-rotation again.
[uiApplication setStatusBarOrientation:self.lockedOrientation
animated:kAnimateContentOrientationChanges];
// Then we can re-enable auto-rotation
self.lockedOrientation = UIInterfaceOrientationUnknown;
// And finally let iOS rotate the root view to match the device orientation
[UIViewController attemptRotationToDeviceOrientation];
}
}
#endif
}
@end