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