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 "qioswindow.h" 41 42#include "qiosapplicationdelegate.h" 43#include "qioscontext.h" 44#include "qiosglobal.h" 45#include "qiosintegration.h" 46#include "qiosscreen.h" 47#include "qiosviewcontroller.h" 48#include "quiview.h" 49 50#include <QtGui/private/qwindow_p.h> 51#include <qpa/qplatformintegration.h> 52 53#import <QuartzCore/CAEAGLLayer.h> 54#ifdef Q_OS_IOS 55#import <QuartzCore/CAMetalLayer.h> 56#endif 57 58#include <QtDebug> 59 60QT_BEGIN_NAMESPACE 61 62QIOSWindow::QIOSWindow(QWindow *window) 63 : QPlatformWindow(window) 64 , m_windowLevel(0) 65{ 66#ifdef Q_OS_IOS 67 if (window->surfaceType() == QSurface::MetalSurface) 68 m_view = [[QUIMetalView alloc] initWithQIOSWindow:this]; 69 else 70#endif 71 m_view = [[QUIView alloc] initWithQIOSWindow:this]; 72 73 connect(qGuiApp, &QGuiApplication::applicationStateChanged, this, &QIOSWindow::applicationStateChanged); 74 75 setParent(QPlatformWindow::parent()); 76 77 // Resolve default window geometry in case it was not set before creating the 78 // platform window. This picks up eg. minimum-size if set, and defaults to 79 // the "maxmized" geometry (even though we're not in that window state). 80 // FIXME: Detect if we apply a maximized geometry and send a window state 81 // change event in that case. 82 m_normalGeometry = initialGeometry(window, QPlatformWindow::geometry(), 83 screen()->availableGeometry().width(), screen()->availableGeometry().height()); 84 85 setWindowState(window->windowStates()); 86 setOpacity(window->opacity()); 87 88 Qt::ScreenOrientation initialOrientation = window->contentOrientation(); 89 if (initialOrientation != Qt::PrimaryOrientation) { 90 // Start up in portrait, then apply possible content orientation, 91 // as per Apple's documentation. 92 dispatch_async(dispatch_get_main_queue(), ^{ 93 handleContentOrientationChange(initialOrientation); 94 }); 95 } 96} 97 98QIOSWindow::~QIOSWindow() 99{ 100 // According to the UIResponder documentation, Cocoa Touch should react to system interruptions 101 // that "might cause the view to be removed from the window" by sending touchesCancelled, but in 102 // practice this doesn't seem to happen when removing the view from its superview. To ensure that 103 // Qt's internal state for touch and mouse handling is kept consistent, we therefor have to force 104 // cancellation of all touch events. 105 [m_view touchesCancelled:[NSSet set] withEvent:0]; 106 107 clearAccessibleCache(); 108 m_view.platformWindow = 0; 109 [m_view removeFromSuperview]; 110 [m_view release]; 111} 112 113 114QSurfaceFormat QIOSWindow::format() const 115{ 116 return window()->requestedFormat(); 117} 118 119 120bool QIOSWindow::blockedByModal() 121{ 122 QWindow *modalWindow = QGuiApplication::modalWindow(); 123 return modalWindow && modalWindow != window(); 124} 125 126void QIOSWindow::setVisible(bool visible) 127{ 128 m_view.hidden = !visible; 129 [m_view setNeedsDisplay]; 130 131 if (!isQtApplication() || !window()->isTopLevel()) 132 return; 133 134 // Since iOS doesn't do window management the way a Qt application 135 // expects, we need to raise and activate windows ourselves: 136 if (visible) 137 updateWindowLevel(); 138 139 if (blockedByModal()) { 140 if (visible) 141 raise(); 142 return; 143 } 144 145 if (visible && shouldAutoActivateWindow()) { 146 if (!window()->property("_q_showWithoutActivating").toBool()) 147 requestActivateWindow(); 148 } else if (!visible && [m_view isActiveWindow]) { 149 // Our window was active/focus window but now hidden, so relinquish 150 // focus to the next possible window in the stack. 151 NSArray<UIView *> *subviews = m_view.viewController.view.subviews; 152 for (int i = int(subviews.count) - 1; i >= 0; --i) { 153 UIView *view = [subviews objectAtIndex:i]; 154 if (view.hidden) 155 continue; 156 157 QWindow *w = view.qwindow; 158 if (!w || !w->isTopLevel()) 159 continue; 160 161 QIOSWindow *iosWindow = static_cast<QIOSWindow *>(w->handle()); 162 if (!iosWindow->shouldAutoActivateWindow()) 163 continue; 164 165 iosWindow->requestActivateWindow(); 166 break; 167 } 168 } 169} 170 171bool QIOSWindow::shouldAutoActivateWindow() const 172{ 173 if (![m_view canBecomeFirstResponder]) 174 return false; 175 176 // We don't want to do automatic window activation for popup windows 177 // that are unlikely to contain editable controls (to avoid hiding 178 // the keyboard while the popup is showing) 179 const Qt::WindowType type = window()->type(); 180 return (type != Qt::Popup && type != Qt::ToolTip) || !window()->isActive(); 181} 182 183void QIOSWindow::setOpacity(qreal level) 184{ 185 m_view.alpha = qBound(0.0, level, 1.0); 186} 187 188void QIOSWindow::setGeometry(const QRect &rect) 189{ 190 m_normalGeometry = rect; 191 192 if (window()->windowState() != Qt::WindowNoState) { 193 QPlatformWindow::setGeometry(rect); 194 195 // The layout will realize the requested geometry was not applied, and 196 // send geometry-change events that match the actual geometry. 197 [m_view setNeedsLayout]; 198 199 if (window()->inherits("QWidgetWindow")) { 200 // QWidget wrongly assumes that setGeometry resets the window 201 // state back to Qt::NoWindowState, so we need to inform it that 202 // that his is not the case by re-issuing the current window state. 203 QWindowSystemInterface::handleWindowStateChanged(window(), window()->windowState()); 204 205 // It also needs to be told immediately that the geometry it requested 206 // did not apply, otherwise it will continue on as if it did, instead 207 // of waiting for a resize event. 208 [m_view layoutIfNeeded]; 209 } 210 211 return; 212 } 213 214 applyGeometry(rect); 215} 216 217void QIOSWindow::applyGeometry(const QRect &rect) 218{ 219 // Geometry changes are asynchronous, but QWindow::geometry() is 220 // expected to report back the 'requested geometry' until we get 221 // a callback with the updated geometry from the window system. 222 // The baseclass takes care of persisting this for us. 223 QPlatformWindow::setGeometry(rect); 224 225 m_view.frame = rect.toCGRect(); 226 227 // iOS will automatically trigger -[layoutSubviews:] for resize, 228 // but not for move, so we force it just in case. 229 [m_view setNeedsLayout]; 230 231 if (window()->inherits("QWidgetWindow")) 232 [m_view layoutIfNeeded]; 233} 234 235QMargins QIOSWindow::safeAreaMargins() const 236{ 237 UIEdgeInsets safeAreaInsets = m_view.qt_safeAreaInsets; 238 return QMargins(safeAreaInsets.left, safeAreaInsets.top, 239 safeAreaInsets.right, safeAreaInsets.bottom); 240} 241 242bool QIOSWindow::isExposed() const 243{ 244 return qApp->applicationState() != Qt::ApplicationSuspended 245 && window()->isVisible() && !window()->geometry().isEmpty(); 246} 247 248void QIOSWindow::setWindowState(Qt::WindowStates state) 249{ 250 // Update the QWindow representation straight away, so that 251 // we can update the statusbar visibility based on the new 252 // state before applying geometry changes. 253 qt_window_private(window())->windowState = state; 254 255 if (window()->isTopLevel() && window()->isVisible() && window()->isActive()) 256 [m_view.qtViewController updateProperties]; 257 258 if (state & Qt::WindowMinimized) { 259 applyGeometry(QRect()); 260 } else if (state & (Qt::WindowFullScreen | Qt::WindowMaximized)) { 261 // When an application is in split-view mode, the UIScreen still has the 262 // same geometry, but the UIWindow is resized to the area reserved for the 263 // application. We use this to constrain the geometry used when applying the 264 // fullscreen or maximized window states. Note that we do not do this 265 // in applyGeometry(), as we don't want to artificially limit window 266 // placement "outside" of the screen bounds if that's what the user wants. 267 268 QRect uiWindowBounds = QRectF::fromCGRect(m_view.window.bounds).toRect(); 269 QRect fullscreenGeometry = screen()->geometry().intersected(uiWindowBounds); 270 QRect maximizedGeometry = window()->flags() & Qt::MaximizeUsingFullscreenGeometryHint ? 271 fullscreenGeometry : screen()->availableGeometry().intersected(uiWindowBounds); 272 273 if (state & Qt::WindowFullScreen) 274 applyGeometry(fullscreenGeometry); 275 else 276 applyGeometry(maximizedGeometry); 277 } else { 278 applyGeometry(m_normalGeometry); 279 } 280} 281 282void QIOSWindow::setParent(const QPlatformWindow *parentWindow) 283{ 284 UIView *parentView = parentWindow ? reinterpret_cast<UIView *>(parentWindow->winId()) 285 : isQtApplication() ? static_cast<QIOSScreen *>(screen())->uiWindow().rootViewController.view : 0; 286 287 [parentView addSubview:m_view]; 288} 289 290void QIOSWindow::requestActivateWindow() 291{ 292 // Note that several windows can be active at the same time if they exist in the same 293 // hierarchy (transient children). But only one window can be QGuiApplication::focusWindow(). 294 // Dispite the name, 'requestActivateWindow' means raise and transfer focus to the window: 295 if (blockedByModal()) 296 return; 297 298 Q_ASSERT(m_view.window); 299 [m_view.window makeKeyWindow]; 300 [m_view becomeFirstResponder]; 301 302 if (window()->isTopLevel()) 303 raise(); 304} 305 306void QIOSWindow::raiseOrLower(bool raise) 307{ 308 // Re-insert m_view at the correct index among its sibling views 309 // (QWindows) according to their current m_windowLevel: 310 if (!isQtApplication()) 311 return; 312 313 NSArray<UIView *> *subviews = m_view.superview.subviews; 314 if (subviews.count == 1) 315 return; 316 317 for (int i = int(subviews.count) - 1; i >= 0; --i) { 318 UIView *view = static_cast<UIView *>([subviews objectAtIndex:i]); 319 if (view.hidden || view == m_view || !view.qwindow) 320 continue; 321 int level = static_cast<QIOSWindow *>(view.qwindow->handle())->m_windowLevel; 322 if (m_windowLevel > level || (raise && m_windowLevel == level)) { 323 [m_view.superview insertSubview:m_view aboveSubview:view]; 324 return; 325 } 326 } 327 [m_view.superview insertSubview:m_view atIndex:0]; 328} 329 330void QIOSWindow::updateWindowLevel() 331{ 332 Qt::WindowType type = window()->type(); 333 334 if (type == Qt::ToolTip) 335 m_windowLevel = 120; 336 else if (window()->flags() & Qt::WindowStaysOnTopHint) 337 m_windowLevel = 100; 338 else if (window()->isModal()) 339 m_windowLevel = 40; 340 else if (type == Qt::Popup) 341 m_windowLevel = 30; 342 else if (type == Qt::SplashScreen) 343 m_windowLevel = 20; 344 else if (type == Qt::Tool) 345 m_windowLevel = 10; 346 else 347 m_windowLevel = 0; 348 349 // A window should be in at least the same m_windowLevel as its parent: 350 QWindow *transientParent = window()->transientParent(); 351 QIOSWindow *transientParentWindow = transientParent ? static_cast<QIOSWindow *>(transientParent->handle()) : 0; 352 if (transientParentWindow) 353 m_windowLevel = qMax(transientParentWindow->m_windowLevel, m_windowLevel); 354} 355 356void QIOSWindow::handleContentOrientationChange(Qt::ScreenOrientation orientation) 357{ 358 // Update the QWindow representation straight away, so that 359 // we can update the statusbar orientation based on the new 360 // content orientation. 361 qt_window_private(window())->contentOrientation = orientation; 362 363 [m_view.qtViewController updateProperties]; 364} 365 366void QIOSWindow::applicationStateChanged(Qt::ApplicationState) 367{ 368 if (window()->isExposed() != isExposed()) 369 [m_view sendUpdatedExposeEvent]; 370} 371 372qreal QIOSWindow::devicePixelRatio() const 373{ 374 return m_view.contentScaleFactor; 375} 376 377void QIOSWindow::clearAccessibleCache() 378{ 379 [m_view clearAccessibleCache]; 380} 381 382void QIOSWindow::requestUpdate() 383{ 384 static_cast<QIOSScreen *>(screen())->setUpdatesPaused(false); 385} 386 387CAEAGLLayer *QIOSWindow::eaglLayer() const 388{ 389 Q_ASSERT([m_view.layer isKindOfClass:[CAEAGLLayer class]]); 390 return static_cast<CAEAGLLayer *>(m_view.layer); 391} 392 393#ifndef QT_NO_DEBUG_STREAM 394QDebug operator<<(QDebug debug, const QIOSWindow *window) 395{ 396 QDebugStateSaver saver(debug); 397 debug.nospace(); 398 debug << "QIOSWindow(" << (const void *)window; 399 if (window) 400 debug << ", window=" << window->window(); 401 debug << ')'; 402 return debug; 403} 404#endif // !QT_NO_DEBUG_STREAM 405 406#include "moc_qioswindow.cpp" 407 408QT_END_NAMESPACE 409