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 const Qt::WindowType type = window()->type();
168
169 // Tooltips are documented to not activate, and activating one would cause
170 // it to immediately hide itself again, as QTipLabel treats the resulting
171 // focus/activation change as a signal to dismiss the tooltip.
172 if (type == Qt::ToolTip)
173 return false;
174
175 // We don't want to do automatic window activation for popup windows
176 // that are unlikely to contain editable controls (to avoid hiding
177 // the keyboard while the popup is showing)
178 return type != Qt::Popup || !window()->isActive();
179}
180
181void QIOSWindow::setOpacity(qreal level)
182{
183 m_view.alpha = qBound(0.0, level, 1.0);
184}
185
186void QIOSWindow::setGeometry(const QRect &rect)
187{
188 m_normalGeometry = rect;
189
190 if (window()->windowState() != Qt::WindowNoState) {
191 QPlatformWindow::setGeometry(rect);
192
193 // The layout will realize the requested geometry was not applied, and
194 // send geometry-change events that match the actual geometry.
195 [m_view setNeedsLayout];
196
197 if (window()->inherits("QWidgetWindow")) {
198 // QWidget wrongly assumes that setGeometry resets the window
199 // state back to Qt::NoWindowState, so we need to inform it that
200 // that his is not the case by re-issuing the current window state.
201 QWindowSystemInterface::handleWindowStateChanged(window(), window()->windowState());
202
203 // It also needs to be told immediately that the geometry it requested
204 // did not apply, otherwise it will continue on as if it did, instead
205 // of waiting for a resize event.
206 [m_view layoutIfNeeded];
207 }
208
209 return;
210 }
211
212 applyGeometry(rect);
213}
214
215void QIOSWindow::applyGeometry(const QRect &rect)
216{
217 // Geometry changes are asynchronous, but QWindow::geometry() is
218 // expected to report back the 'requested geometry' until we get
219 // a callback with the updated geometry from the window system.
220 // The baseclass takes care of persisting this for us.
221 QPlatformWindow::setGeometry(rect);
222
223 m_view.frame = rect.toCGRect();
224
225 // iOS will automatically trigger -[layoutSubviews:] for resize,
226 // but not for move, so we force it just in case.
227 [m_view setNeedsLayout];
228
229 if (window()->inherits("QWidgetWindow"))
230 [m_view layoutIfNeeded];
231}
232
233QMargins QIOSWindow::safeAreaMargins() const
234{
235 UIEdgeInsets safeAreaInsets = m_view.safeAreaInsets;
236 return QMargins(safeAreaInsets.left, safeAreaInsets.top,
237 safeAreaInsets.right, safeAreaInsets.bottom);
238}
239
240bool QIOSWindow::isExposed() const
241{
242 return qApp->applicationState() != Qt::ApplicationSuspended
243 && window()->isVisible() && !window()->geometry().isEmpty();
244}
245
246void QIOSWindow::setWindowState(Qt::WindowStates state)
247{
248 // Update the QWindow representation straight away, so that
249 // we can update the statusbar visibility based on the new
250 // state before applying geometry changes.
251 qt_window_private(window())->windowState = state;
252
253 if (window()->isTopLevel() && window()->isVisible() && window()->isActive())
254 [m_view.qtViewController updateStatusBarProperties];
255
256 if (state & Qt::WindowMinimized) {
257 applyGeometry(QRect());
258 } else if (state & (Qt::WindowFullScreen | Qt::WindowMaximized)) {
259 QRect uiWindowBounds = QRectF::fromCGRect(m_view.window.bounds).toRect();
260 if (NSProcessInfo.processInfo.iOSAppOnMac) {
261 // iOS apps running as "Designed for iPad" on macOS do not match
262 // our current window management implementation where a single
263 // UIWindow is tied to a single screen. And even if we're on the
264 // right screen, the UIScreen does not account for the 77% scale
265 // of the UIUserInterfaceIdiomPad environment, so we can't use
266 // it to clamp the window geometry. Instead just use the UIWindow
267 // directly, which represents our "screen".
268 applyGeometry(uiWindowBounds);
269 } else if (isRunningOnVisionOS()) {
270 // On visionOS there is no concept of a screen, and hence no concept of
271 // screen-relative system UI that we should keep top level windows away
272 // from, so don't apply the UIWindow safe area insets to the screen.
273 applyGeometry(uiWindowBounds);
274 } else {
275 QRect fullscreenGeometry = screen()->geometry();
276 QRect maximizedGeometry = fullscreenGeometry;
277
278#if !defined(Q_OS_VISIONOS)
279 if (!(window()->flags() & Qt::ExpandedClientAreaHint)) {
280 // If the safe area margins reflect the screen's outer edges,
281 // then reduce the maximized geometry accordingly. Otherwise
282 // leave it as is, and assume the client will take the safe
283 // are margins into account explicitly.
284 UIScreen *uiScreen = m_view.window.windowScene.screen;
285 UIEdgeInsets safeAreaInsets = m_view.window.safeAreaInsets;
286 if (m_view.window.bounds.size.width == uiScreen.bounds.size.width)
287 maximizedGeometry.adjust(safeAreaInsets.left, 0, -safeAreaInsets.right, 0);
288 if (m_view.window.bounds.size.height == uiScreen.bounds.size.height)
289 maximizedGeometry.adjust(0, safeAreaInsets.top, 0, -safeAreaInsets.bottom);
290 }
291#endif
292
293 if (m_view.window) {
294 // On application startup, during main(), we don't have a UIWindow yet (because
295 // the UIWindowScene has not been connected yet), but once the scene has been
296 // connected and we have a UIWindow we can adjust the maximized/fullscreen size
297 // to account for split-view or floating window mode, where the UIWindow is
298 // smaller than the screen.
299 fullscreenGeometry = fullscreenGeometry.intersected(uiWindowBounds);
300 maximizedGeometry = maximizedGeometry.intersected(uiWindowBounds);
301 }
302
303 if (state & Qt::WindowFullScreen)
304 applyGeometry(fullscreenGeometry);
305 else
306 applyGeometry(maximizedGeometry);
307 }
308 } else {
309 applyGeometry(m_normalGeometry);
310 }
311}
312
313void QIOSWindow::setParent(const QPlatformWindow *parentWindow)
314{
315 UIView *superview = nullptr;
316 if (parentWindow)
317 superview = reinterpret_cast<UIView *>(parentWindow->winId());
318 else if (isQtApplication() && !isForeignWindow())
319 superview = rootViewForScreen(window()->screen()->handle());
320
321 if (superview)
322 [superview addSubview:m_view];
323 else if (quiview_cast(m_view.superview))
324 [m_view removeFromSuperview];
325}
326
327void QIOSWindow::requestActivateWindow()
328{
329 // Note that several windows can be active at the same time if they exist in the same
330 // hierarchy (transient children). But only one window can be QGuiApplication::focusWindow().
331 // Despite the name, 'requestActivateWindow' means raise and transfer focus to the window:
332 if (blockedByModal())
333 return;
334
335 [m_view.window makeKeyWindow];
336 [m_view becomeFirstResponder];
337
338 if (window()->isTopLevel())
339 raise();
340}
341
342void QIOSWindow::raiseOrLower(bool raise)
343{
345 return;
346
347 NSArray<UIView *> *subviews = m_view.superview.subviews;
348 if (subviews.count == 1)
349 return;
350
351 if (m_view.superview == m_view.qtViewController.view) {
352 // We're a top level window, so we need to take window
353 // levels into account.
354 for (int i = int(subviews.count) - 1; i >= 0; --i) {
355 UIView *view = static_cast<UIView *>([subviews objectAtIndex:i]);
356 if (view.hidden || view == m_view || !view.qwindow)
357 continue;
358 int level = static_cast<QIOSWindow *>(view.qwindow->handle())->windowLevel();
359 if (windowLevel() > level || (raise && windowLevel() == level)) {
360 [m_view.superview insertSubview:m_view aboveSubview:view];
361 return;
362 }
363 }
364 [m_view.superview insertSubview:m_view atIndex:0];
365 } else {
366 // Child window, or embedded into a non-Qt view controller
367 if (raise)
368 [m_view.superview bringSubviewToFront:m_view];
369 else
370 [m_view.superview sendSubviewToBack:m_view];
371 }
372}
373
374int QIOSWindow::windowLevel() const
375{
376 Qt::WindowType type = window()->type();
377
378 int level = 0;
379
380 if (type == Qt::ToolTip)
381 level = 120;
382 else if (window()->flags() & Qt::WindowStaysOnTopHint)
383 level = 100;
384 else if (window()->isModal())
385 level = 40;
386 else if (type == Qt::Popup)
387 level = 30;
388 else if (type == Qt::SplashScreen)
389 level = 20;
390 else if (type == Qt::Tool)
391 level = 10;
392 else
393 level = 0;
394
395 // A window should be in at least the same window level as its parent
396 QWindow *transientParent = window()->transientParent();
397 QIOSWindow *transientParentWindow = transientParent ? static_cast<QIOSWindow *>(transientParent->handle()) : 0;
398 if (transientParentWindow)
399 level = qMax(transientParentWindow->windowLevel(), level);
400
401 return level;
402}
403
404void QIOSWindow::applicationStateChanged(Qt::ApplicationState)
405{
406 if (isForeignWindow())
407 return;
408
409 if (window()->isExposed() != isExposed())
410 [quiview_cast(m_view) sendUpdatedExposeEvent];
411}
412
413qreal QIOSWindow::devicePixelRatio() const
414{
415#if !defined(Q_OS_VISIONOS)
416 // If the view has not yet been added to a screen, it will not
417 // pick up its device pixel ratio, so we need to do so manually
418 // based on the screen we think the window will be added to.
419 if (!m_view.window.windowScene.screen)
420 return screen()->devicePixelRatio();
421#endif
422
423 // Otherwise we can rely on the content scale factor
424 return m_view.contentScaleFactor;
425}
426
427void QIOSWindow::clearAccessibleCache()
428{
429 if (isForeignWindow())
430 return;
431
432 [quiview_cast(m_view) clearAccessibleCache];
433}
434
435void QIOSWindow::requestUpdate()
436{
437 static_cast<QIOSScreen *>(screen())->setUpdatesPaused(false);
438}
439
440void QIOSWindow::setMask(const QRegion &region)
441{
442 if (!region.isEmpty()) {
443 QCFType<CGMutablePathRef> maskPath = CGPathCreateMutable();
444 for (const QRect &r : region)
445 CGPathAddRect(maskPath, nullptr, r.toCGRect());
446 CAShapeLayer *maskLayer = [CAShapeLayer layer];
447 maskLayer.path = maskPath;
448 m_view.layer.mask = maskLayer;
449 } else {
450 m_view.layer.mask = nil;
451 }
452}
453
454#if QT_CONFIG(opengl)
455CAEAGLLayer *QIOSWindow::eaglLayer() const
456{
457 Q_ASSERT([m_view.layer isKindOfClass:[CAEAGLLayer class]]);
458 return static_cast<CAEAGLLayer *>(m_view.layer);
459}
460#endif
461
462#ifndef QT_NO_DEBUG_STREAM
463QDebug operator<<(QDebug debug, const QIOSWindow *window)
464{
465 QDebugStateSaver saver(debug);
466 debug.nospace();
467 debug << "QIOSWindow(" << (const void *)window;
468 if (window)
469 debug << ", window=" << window->window();
470 debug << ')';
471 return debug;
472}
473#endif // !QT_NO_DEBUG_STREAM
474
475/*!
476 Returns the view cast to a QUIview if possible.
477
478 If the view is not a QUIview, nil is returned, which is safe to
479 send messages to, effectively making [quiview_cast(view) message]
480 a no-op.
481
482 For extra verbosity and clearer code, please consider checking
483 that the platform window is not a foreign window before using
484 this cast, via QPlatformWindow::isForeignWindow().
485
486 Do not use this method solely to check for foreign windows, as
487 that will make the code harder to read for people not working
488 primarily on iOS, who do not know the difference between the
489 UIView and QUIView cases.
490*/
491QUIView *quiview_cast(UIView *view)
492{
493 return qt_objc_cast<QUIView *>(view);
494}
495
496bool QIOSWindow::isForeignWindow() const
497{
498 return ![m_view isKindOfClass:QUIView.class];
499}
500
501UIView *QIOSWindow::view() const
502{
503 return m_view;
504}
505
506QT_END_NAMESPACE
507
508#include "moc_qioswindow.cpp"
Combined button and popup list for selecting options.
bool isQtApplication()
Definition qiosglobal.mm:21
bool isRunningOnVisionOS()
Definition qiosglobal.mm:32
QDebug operator<<(QDebug debug, const QIOSWindow *window)
QUIView * quiview_cast(UIView *view)
Returns the view cast to a QUIview if possible.
@ defaultWindowHeight
Definition qioswindow.mm:35
@ defaultWindowWidth
Definition qioswindow.mm:34