Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qioswindow.mm
Go to the documentation of this file.
1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:significant reason:default
4
5#include "qioswindow.h"
6
8#include "qiosglobal.h"
10#include "qiosscreen.h"
12#include "quiview.h"
14
15#include <QtCore/private/qcore_mac_p.h>
16
17#include <QtGui/private/qwindow_p.h>
18#include <QtGui/private/qhighdpiscaling_p.h>
19#include <qpa/qplatformintegration.h>
20
21#if QT_CONFIG(opengl)
22#import <QuartzCore/CAEAGLLayer.h>
23#endif
24
25#if QT_CONFIG(metal)
26#import <QuartzCore/CAMetalLayer.h>
27#endif
28
29#include <QtDebug>
30
32
33enum {
36};
37
38QIOSWindow::QIOSWindow(QWindow *window, WId nativeHandle)
39 : QPlatformWindow(window)
40{
41 if (nativeHandle) {
42 m_view = reinterpret_cast<UIView *>(nativeHandle);
43 [m_view retain];
44 } else {
45#if QT_CONFIG(metal)
46 if (window->surfaceType() == QSurface::RasterSurface)
47 window->setSurfaceType(QSurface::MetalSurface);
48
49 if (window->surfaceType() == QSurface::MetalSurface)
50 m_view = [[QUIMetalView alloc] initWithQIOSWindow:this];
51 else
52#endif
53 m_view = [[QUIView alloc] initWithQIOSWindow:this];
54 }
55
56 connect(qGuiApp, &QGuiApplication::applicationStateChanged, this, &QIOSWindow::applicationStateChanged);
57
58 // Always set parent, even if we don't have a parent window,
59 // as we use setParent to reparent top levels into our desktop
60 // manager view.
61 setParent(QPlatformWindow::parent());
62
63 if (!isForeignWindow()) {
64 // Resolve default window geometry in case it was not set before creating the
65 // platform window. This picks up eg. minimum-size if set.
66 m_normalGeometry = initialGeometry(window, QPlatformWindow::geometry(),
67 defaultWindowWidth, defaultWindowHeight);
68
69 setWindowState(window->windowStates());
70 setOpacity(window->opacity());
71 setMask(QHighDpi::toNativeLocalRegion(window->mask(), window));
72 } else {
73 // Pick up essential foreign window state
74 QPlatformWindow::setGeometry(QRectF::fromCGRect(m_view.frame).toRect());
75 }
76
77 Qt::ScreenOrientation initialOrientation = window->contentOrientation();
78 if (initialOrientation != Qt::PrimaryOrientation) {
79 // Start up in portrait, then apply possible content orientation,
80 // as per Apple's documentation.
81 dispatch_async(dispatch_get_main_queue(), ^{
82 handleContentOrientationChange(initialOrientation);
83 });
84 }
85}
86
87QIOSWindow::~QIOSWindow()
88{
89 // According to the UIResponder documentation, Cocoa Touch should react to system interruptions
90 // that "might cause the view to be removed from the window" by sending touchesCancelled, but in
91 // practice this doesn't seem to happen when removing the view from its superview. To ensure that
92 // Qt's internal state for touch and mouse handling is kept consistent, we therefore have to force
93 // cancellation of all touch events.
94 [m_view touchesCancelled:[NSSet set] withEvent:0];
95
96 clearAccessibleCache();
97
98 quiview_cast(m_view).platformWindow = nullptr;
99
100 // Remove from superview, unless we're a foreign window without a
101 // Qt window parent, in which case the foreign window is used as
102 // a window container for a Qt UI hierarchy inside a native UI.
103 if (!(isForeignWindow() && !QPlatformWindow::parent()))
104 [m_view removeFromSuperview];
105
106 [m_view release];
107}
108
109
110QSurfaceFormat QIOSWindow::format() const
111{
112 return window()->requestedFormat();
113}
114
115
116bool QIOSWindow::blockedByModal()
117{
118 QWindow *modalWindow = QGuiApplication::modalWindow();
119 return modalWindow && modalWindow != window();
120}
121
122void QIOSWindow::setVisible(bool visible)
123{
124 m_view.hidden = !visible;
125 [m_view setNeedsDisplay];
126
127 if (!isQtApplication() || !window()->isTopLevel())
128 return;
129
130 if (blockedByModal()) {
131 if (visible)
132 raise();
133 return;
134 }
135
136 if (visible && shouldAutoActivateWindow()) {
137 if (!window()->property("_q_showWithoutActivating").toBool())
138 requestActivateWindow();
139 } else if (!visible && [quiview_cast(m_view) isActiveWindow]) {
140 // Our window was active/focus window but now hidden, so relinquish
141 // focus to the next possible window in the stack.
142 NSArray<UIView *> *subviews = m_view.viewController.view.subviews;
143 for (int i = int(subviews.count) - 1; i >= 0; --i) {
144 UIView *view = [subviews objectAtIndex:i];
145 if (view.hidden)
146 continue;
147
148 QWindow *w = view.qwindow;
149 if (!w || !w->isTopLevel())
150 continue;
151
152 QIOSWindow *iosWindow = static_cast<QIOSWindow *>(w->handle());
153 if (!iosWindow->shouldAutoActivateWindow())
154 continue;
155
156 iosWindow->requestActivateWindow();
157 break;
158 }
159 }
160}
161
162bool QIOSWindow::shouldAutoActivateWindow() const
163{
164 if (![m_view canBecomeFirstResponder])
165 return false;
166
167 // We don't want to do automatic window activation for popup windows
168 // that are unlikely to contain editable controls (to avoid hiding
169 // the keyboard while the popup is showing)
170 const Qt::WindowType type = window()->type();
171 return (type != Qt::Popup && type != Qt::ToolTip) || !window()->isActive();
172}
173
174void QIOSWindow::setOpacity(qreal level)
175{
176 m_view.alpha = qBound(0.0, level, 1.0);
177}
178
179void QIOSWindow::setGeometry(const QRect &rect)
180{
181 m_normalGeometry = rect;
182
183 if (window()->windowState() != Qt::WindowNoState) {
184 QPlatformWindow::setGeometry(rect);
185
186 // The layout will realize the requested geometry was not applied, and
187 // send geometry-change events that match the actual geometry.
188 [m_view setNeedsLayout];
189
190 if (window()->inherits("QWidgetWindow")) {
191 // QWidget wrongly assumes that setGeometry resets the window
192 // state back to Qt::NoWindowState, so we need to inform it that
193 // that his is not the case by re-issuing the current window state.
194 QWindowSystemInterface::handleWindowStateChanged(window(), window()->windowState());
195
196 // It also needs to be told immediately that the geometry it requested
197 // did not apply, otherwise it will continue on as if it did, instead
198 // of waiting for a resize event.
199 [m_view layoutIfNeeded];
200 }
201
202 return;
203 }
204
205 applyGeometry(rect);
206}
207
208void QIOSWindow::applyGeometry(const QRect &rect)
209{
210 // Geometry changes are asynchronous, but QWindow::geometry() is
211 // expected to report back the 'requested geometry' until we get
212 // a callback with the updated geometry from the window system.
213 // The baseclass takes care of persisting this for us.
214 QPlatformWindow::setGeometry(rect);
215
216 m_view.frame = rect.toCGRect();
217
218 // iOS will automatically trigger -[layoutSubviews:] for resize,
219 // but not for move, so we force it just in case.
220 [m_view setNeedsLayout];
221
222 if (window()->inherits("QWidgetWindow"))
223 [m_view layoutIfNeeded];
224}
225
226QMargins QIOSWindow::safeAreaMargins() const
227{
228 UIEdgeInsets safeAreaInsets = m_view.safeAreaInsets;
229 return QMargins(safeAreaInsets.left, safeAreaInsets.top,
230 safeAreaInsets.right, safeAreaInsets.bottom);
231}
232
233bool QIOSWindow::isExposed() const
234{
235 return qApp->applicationState() != Qt::ApplicationSuspended
236 && window()->isVisible() && !window()->geometry().isEmpty();
237}
238
239void QIOSWindow::setWindowState(Qt::WindowStates state)
240{
241 // Update the QWindow representation straight away, so that
242 // we can update the statusbar visibility based on the new
243 // state before applying geometry changes.
244 qt_window_private(window())->windowState = state;
245
246 if (window()->isTopLevel() && window()->isVisible() && window()->isActive())
247 [m_view.qtViewController updateStatusBarProperties];
248
249 if (state & Qt::WindowMinimized) {
250 applyGeometry(QRect());
251 } else if (state & (Qt::WindowFullScreen | Qt::WindowMaximized)) {
252 QRect uiWindowBounds = QRectF::fromCGRect(m_view.window.bounds).toRect();
253 if (NSProcessInfo.processInfo.iOSAppOnMac) {
254 // iOS apps running as "Designed for iPad" on macOS do not match
255 // our current window management implementation where a single
256 // UIWindow is tied to a single screen. And even if we're on the
257 // right screen, the UIScreen does not account for the 77% scale
258 // of the UIUserInterfaceIdiomPad environment, so we can't use
259 // it to clamp the window geometry. Instead just use the UIWindow
260 // directly, which represents our "screen".
261 applyGeometry(uiWindowBounds);
262 } else if (isRunningOnVisionOS()) {
263 // On visionOS there is no concept of a screen, and hence no concept of
264 // screen-relative system UI that we should keep top level windows away
265 // from, so don't apply the UIWindow safe area insets to the screen.
266 applyGeometry(uiWindowBounds);
267 } else {
268 QRect fullscreenGeometry = screen()->geometry();
269 QRect maximizedGeometry = fullscreenGeometry;
270
271#if !defined(Q_OS_VISIONOS)
272 if (!(window()->flags() & Qt::ExpandedClientAreaHint)) {
273 // If the safe area margins reflect the screen's outer edges,
274 // then reduce the maximized geometry accordingly. Otherwise
275 // leave it as is, and assume the client will take the safe
276 // are margins into account explicitly.
277 UIScreen *uiScreen = m_view.window.windowScene.screen;
278 UIEdgeInsets safeAreaInsets = m_view.window.safeAreaInsets;
279 if (m_view.window.bounds.size.width == uiScreen.bounds.size.width)
280 maximizedGeometry.adjust(safeAreaInsets.left, 0, -safeAreaInsets.right, 0);
281 if (m_view.window.bounds.size.height == uiScreen.bounds.size.height)
282 maximizedGeometry.adjust(0, safeAreaInsets.top, 0, -safeAreaInsets.bottom);
283 }
284#endif
285
286 if (m_view.window) {
287 // On application startup, during main(), we don't have a UIWindow yet (because
288 // the UIWindowScene has not been connected yet), but once the scene has been
289 // connected and we have a UIWindow we can adjust the maximized/fullscreen size
290 // to account for split-view or floating window mode, where the UIWindow is
291 // smaller than the screen.
292 fullscreenGeometry = fullscreenGeometry.intersected(uiWindowBounds);
293 maximizedGeometry = maximizedGeometry.intersected(uiWindowBounds);
294 }
295
296 if (state & Qt::WindowFullScreen)
297 applyGeometry(fullscreenGeometry);
298 else
299 applyGeometry(maximizedGeometry);
300 }
301 } else {
302 applyGeometry(m_normalGeometry);
303 }
304}
305
306void QIOSWindow::setParent(const QPlatformWindow *parentWindow)
307{
308 UIView *superview = nullptr;
309 if (parentWindow)
310 superview = reinterpret_cast<UIView *>(parentWindow->winId());
311 else if (isQtApplication() && !isForeignWindow())
312 superview = rootViewForScreen(window()->screen()->handle());
313
314 if (superview)
315 [superview addSubview:m_view];
316 else if (quiview_cast(m_view.superview))
317 [m_view removeFromSuperview];
318}
319
320void QIOSWindow::requestActivateWindow()
321{
322 // Note that several windows can be active at the same time if they exist in the same
323 // hierarchy (transient children). But only one window can be QGuiApplication::focusWindow().
324 // Despite the name, 'requestActivateWindow' means raise and transfer focus to the window:
325 if (blockedByModal())
326 return;
327
328 [m_view.window makeKeyWindow];
329 [m_view becomeFirstResponder];
330
331 if (window()->isTopLevel())
332 raise();
333}
334
335void QIOSWindow::raiseOrLower(bool raise)
336{
338 return;
339
340 NSArray<UIView *> *subviews = m_view.superview.subviews;
341 if (subviews.count == 1)
342 return;
343
344 if (m_view.superview == m_view.qtViewController.view) {
345 // We're a top level window, so we need to take window
346 // levels into account.
347 for (int i = int(subviews.count) - 1; i >= 0; --i) {
348 UIView *view = static_cast<UIView *>([subviews objectAtIndex:i]);
349 if (view.hidden || view == m_view || !view.qwindow)
350 continue;
351 int level = static_cast<QIOSWindow *>(view.qwindow->handle())->windowLevel();
352 if (windowLevel() > level || (raise && windowLevel() == level)) {
353 [m_view.superview insertSubview:m_view aboveSubview:view];
354 return;
355 }
356 }
357 [m_view.superview insertSubview:m_view atIndex:0];
358 } else {
359 // Child window, or embedded into a non-Qt view controller
360 if (raise)
361 [m_view.superview bringSubviewToFront:m_view];
362 else
363 [m_view.superview sendSubviewToBack:m_view];
364 }
365}
366
367int QIOSWindow::windowLevel() const
368{
369 Qt::WindowType type = window()->type();
370
371 int level = 0;
372
373 if (type == Qt::ToolTip)
374 level = 120;
375 else if (window()->flags() & Qt::WindowStaysOnTopHint)
376 level = 100;
377 else if (window()->isModal())
378 level = 40;
379 else if (type == Qt::Popup)
380 level = 30;
381 else if (type == Qt::SplashScreen)
382 level = 20;
383 else if (type == Qt::Tool)
384 level = 10;
385 else
386 level = 0;
387
388 // A window should be in at least the same window level as its parent
389 QWindow *transientParent = window()->transientParent();
390 QIOSWindow *transientParentWindow = transientParent ? static_cast<QIOSWindow *>(transientParent->handle()) : 0;
391 if (transientParentWindow)
392 level = qMax(transientParentWindow->windowLevel(), level);
393
394 return level;
395}
396
397void QIOSWindow::applicationStateChanged(Qt::ApplicationState)
398{
399 if (isForeignWindow())
400 return;
401
402 if (window()->isExposed() != isExposed())
403 [quiview_cast(m_view) sendUpdatedExposeEvent];
404}
405
406qreal QIOSWindow::devicePixelRatio() const
407{
408#if !defined(Q_OS_VISIONOS)
409 // If the view has not yet been added to a screen, it will not
410 // pick up its device pixel ratio, so we need to do so manually
411 // based on the screen we think the window will be added to.
412 if (!m_view.window.windowScene.screen)
413 return screen()->devicePixelRatio();
414#endif
415
416 // Otherwise we can rely on the content scale factor
417 return m_view.contentScaleFactor;
418}
419
420void QIOSWindow::clearAccessibleCache()
421{
422 if (isForeignWindow())
423 return;
424
425 [quiview_cast(m_view) clearAccessibleCache];
426}
427
428void QIOSWindow::requestUpdate()
429{
430 static_cast<QIOSScreen *>(screen())->setUpdatesPaused(false);
431}
432
433void QIOSWindow::setMask(const QRegion &region)
434{
435 if (!region.isEmpty()) {
436 QCFType<CGMutablePathRef> maskPath = CGPathCreateMutable();
437 for (const QRect &r : region)
438 CGPathAddRect(maskPath, nullptr, r.toCGRect());
439 CAShapeLayer *maskLayer = [CAShapeLayer layer];
440 maskLayer.path = maskPath;
441 m_view.layer.mask = maskLayer;
442 } else {
443 m_view.layer.mask = nil;
444 }
445}
446
447#if QT_CONFIG(opengl)
448CAEAGLLayer *QIOSWindow::eaglLayer() const
449{
450 Q_ASSERT([m_view.layer isKindOfClass:[CAEAGLLayer class]]);
451 return static_cast<CAEAGLLayer *>(m_view.layer);
452}
453#endif
454
455#ifndef QT_NO_DEBUG_STREAM
456QDebug operator<<(QDebug debug, const QIOSWindow *window)
457{
458 QDebugStateSaver saver(debug);
459 debug.nospace();
460 debug << "QIOSWindow(" << (const void *)window;
461 if (window)
462 debug << ", window=" << window->window();
463 debug << ')';
464 return debug;
465}
466#endif // !QT_NO_DEBUG_STREAM
467
468/*!
469 Returns the view cast to a QUIview if possible.
470
471 If the view is not a QUIview, nil is returned, which is safe to
472 send messages to, effectively making [quiview_cast(view) message]
473 a no-op.
474
475 For extra verbosity and clearer code, please consider checking
476 that the platform window is not a foreign window before using
477 this cast, via QPlatformWindow::isForeignWindow().
478
479 Do not use this method solely to check for foreign windows, as
480 that will make the code harder to read for people not working
481 primarily on iOS, who do not know the difference between the
482 UIView and QUIView cases.
483*/
484QUIView *quiview_cast(UIView *view)
485{
486 return qt_objc_cast<QUIView *>(view);
487}
488
489bool QIOSWindow::isForeignWindow() const
490{
491 return ![m_view isKindOfClass:QUIView.class];
492}
493
494UIView *QIOSWindow::view() const
495{
496 return m_view;
497}
498
499QT_END_NAMESPACE
500
501#include "moc_qioswindow.cpp"
bool isQtApplication()
Definition qiosglobal.mm:21
bool isRunningOnVisionOS()
Definition qiosglobal.mm:32
QDebug operator<<(QDebug debug, const QIOSWindow *window)
@ defaultWindowHeight
Definition qioswindow.mm:35
@ defaultWindowWidth
Definition qioswindow.mm:34
QUIView * quiview_cast(UIView *view)
Returns the view cast to a QUIview if possible.