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