From ba44cdae38406c429c7fb43863a6883bd0f79cf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tor=20Arne=20Vestb=C3=B8?= Date: Fri, 1 Dec 2017 19:03:05 +0100 Subject: [PATCH] Teach QPlatformWindow about safe area margins and implement for iOS The safe area margins of a window represent the area that is safe to place content within, without intersecting areas of the screen where system UI is placed, or where a screen bezel may cover the content. QWidget will incorporate the safe area margins into its contents margins, so that they are are never smaller than the safe area margins. This can be disabled by unsetting the Qt::WA_ContentsMarginsRespectsSafeArea widget attribute, which is set by default. QLayouts will automatically use the contents area of a widget for their layout, unless the Qt::WA_LayoutOnEntireRect attribute has been set. This can be used, along with a contents margin of 0 on the actual layout, to allow e.g. a background image to underlay the status bar and other system areas on an iOS device, while still allowing child widgets of that background to be inset based on the safe area. [ChangeLog][iOS/tvOS] Qt will now take the safe area margins of the device into account when computing layouts for QtWidgets. Change-Id: Ife3827ab663f0625c1451e75b14fb8eeffb00754 Reviewed-by: Richard Moe Gustavsen --- src/corelib/global/qnamespace.h | 2 + src/gui/kernel/qguiapplication.cpp | 14 ++ src/gui/kernel/qguiapplication_p.h | 2 + src/gui/kernel/qplatformwindow.cpp | 10 ++ src/gui/kernel/qplatformwindow.h | 1 + src/gui/kernel/qwindow_p.h | 2 + src/gui/kernel/qwindowsysteminterface.cpp | 7 + src/gui/kernel/qwindowsysteminterface.h | 3 + src/gui/kernel/qwindowsysteminterface_p.h | 12 +- src/plugins/platforms/ios/qioswindow.h | 2 + src/plugins/platforms/ios/qioswindow.mm | 7 + src/plugins/platforms/ios/quiview.mm | 39 ++++++ src/widgets/kernel/qwidget.cpp | 154 +++++++++++++++++----- src/widgets/kernel/qwidget_p.h | 3 + src/widgets/kernel/qwidgetwindow.cpp | 7 + 15 files changed, 234 insertions(+), 31 deletions(-) diff --git a/src/corelib/global/qnamespace.h b/src/corelib/global/qnamespace.h index da44c01594b..bd6b667aabb 100644 --- a/src/corelib/global/qnamespace.h +++ b/src/corelib/global/qnamespace.h @@ -474,6 +474,8 @@ public: WA_TabletTracking = 129, + WA_ContentsMarginsRespectsSafeArea = 130, + // Add new attributes before this line WA_AttributeCount }; diff --git a/src/gui/kernel/qguiapplication.cpp b/src/gui/kernel/qguiapplication.cpp index 6d37014b38e..cff11367f7e 100644 --- a/src/gui/kernel/qguiapplication.cpp +++ b/src/gui/kernel/qguiapplication.cpp @@ -1759,6 +1759,9 @@ void QGuiApplicationPrivate::processWindowSystemEvent(QWindowSystemInterfacePriv case QWindowSystemInterfacePrivate::WindowScreenChanged: QGuiApplicationPrivate::processWindowScreenChangedEvent(static_cast(e)); break; + case QWindowSystemInterfacePrivate::SafeAreaMarginsChanged: + QGuiApplicationPrivate::processSafeAreaMarginsChangedEvent(static_cast(e)); + break; case QWindowSystemInterfacePrivate::ApplicationStateChanged: { QWindowSystemInterfacePrivate::ApplicationStateChangedEvent * changeEvent = static_cast(e); QGuiApplicationPrivate::setApplicationState(changeEvent->newState, changeEvent->forcePropagate); } @@ -2212,6 +2215,17 @@ void QGuiApplicationPrivate::processWindowScreenChangedEvent(QWindowSystemInterf } } +void QGuiApplicationPrivate::processSafeAreaMarginsChangedEvent(QWindowSystemInterfacePrivate::SafeAreaMarginsChangedEvent *wse) +{ + if (wse->window.isNull()) + return; + + // Handle by forwarding directly to QWindowPrivate, instead of sending spontaneous + // QEvent like most other functions, as there's no QEvent type for the safe area + // change, and we don't want to add one until we know that this is a good API. + qt_window_private(wse->window)->processSafeAreaMarginsChanged(); +} + void QGuiApplicationPrivate::processThemeChanged(QWindowSystemInterfacePrivate::ThemeChangeEvent *tce) { if (self) diff --git a/src/gui/kernel/qguiapplication_p.h b/src/gui/kernel/qguiapplication_p.h index 3804667ef3a..d67ab61ff89 100644 --- a/src/gui/kernel/qguiapplication_p.h +++ b/src/gui/kernel/qguiapplication_p.h @@ -133,6 +133,8 @@ public: static void processWindowStateChangedEvent(QWindowSystemInterfacePrivate::WindowStateChangedEvent *e); static void processWindowScreenChangedEvent(QWindowSystemInterfacePrivate::WindowScreenChangedEvent *e); + static void processSafeAreaMarginsChangedEvent(QWindowSystemInterfacePrivate::SafeAreaMarginsChangedEvent *e); + static void processWindowSystemEvent(QWindowSystemInterfacePrivate::WindowSystemEvent *e); static void updateFilteredScreenOrientation(QScreen *screen); diff --git a/src/gui/kernel/qplatformwindow.cpp b/src/gui/kernel/qplatformwindow.cpp index 5062bd1e77e..23e492ee7a4 100644 --- a/src/gui/kernel/qplatformwindow.cpp +++ b/src/gui/kernel/qplatformwindow.cpp @@ -146,6 +146,16 @@ QMargins QPlatformWindow::frameMargins() const return QMargins(); } +/*! + The safe area margins of a window represent the area that is safe to + place content within, without intersecting areas of the screen where + system UI is placed, or where a screen bezel may cover the content. +*/ +QMargins QPlatformWindow::safeAreaMargins() const +{ + return QMargins(); +} + /*! Reimplemented in subclasses to show the surface if \a visible is \c true, and hide it if \a visible is \c false. diff --git a/src/gui/kernel/qplatformwindow.h b/src/gui/kernel/qplatformwindow.h index 8af8791bb4a..2bee55c7e02 100644 --- a/src/gui/kernel/qplatformwindow.h +++ b/src/gui/kernel/qplatformwindow.h @@ -86,6 +86,7 @@ public: virtual QRect normalGeometry() const; virtual QMargins frameMargins() const; + virtual QMargins safeAreaMargins() const; virtual void setVisible(bool visible); virtual void setWindowFlags(Qt::WindowFlags flags); diff --git a/src/gui/kernel/qwindow_p.h b/src/gui/kernel/qwindow_p.h index dd282a671da..568aa1e2fce 100644 --- a/src/gui/kernel/qwindow_p.h +++ b/src/gui/kernel/qwindow_p.h @@ -147,6 +147,8 @@ public: virtual void clearFocusObject(); virtual QRectF closestAcceptableGeometry(const QRectF &rect) const; + virtual void processSafeAreaMarginsChanged() {}; + bool isPopup() const { return (windowFlags & Qt::WindowType_Mask) == Qt::Popup; } static QWindowPrivate *get(QWindow *window) { return window->d_func(); } diff --git a/src/gui/kernel/qwindowsysteminterface.cpp b/src/gui/kernel/qwindowsysteminterface.cpp index 3f27094845c..13f45d236ea 100644 --- a/src/gui/kernel/qwindowsysteminterface.cpp +++ b/src/gui/kernel/qwindowsysteminterface.cpp @@ -265,6 +265,13 @@ void QWindowSystemInterface::handleWindowScreenChanged(QWindow *window, QScreen QWindowSystemInterfacePrivate::handleWindowSystemEvent(e); } +QT_DEFINE_QPA_EVENT_HANDLER(void, handleSafeAreaMarginsChanged, QWindow *window) +{ + QWindowSystemInterfacePrivate::SafeAreaMarginsChangedEvent *e = + new QWindowSystemInterfacePrivate::SafeAreaMarginsChangedEvent(window); + QWindowSystemInterfacePrivate::handleWindowSystemEvent(e); +} + QT_DEFINE_QPA_EVENT_HANDLER(void, handleApplicationStateChanged, Qt::ApplicationState newState, bool forcePropagate) { Q_ASSERT(QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::ApplicationState)); diff --git a/src/gui/kernel/qwindowsysteminterface.h b/src/gui/kernel/qwindowsysteminterface.h index e91c79749d1..fb428233abe 100644 --- a/src/gui/kernel/qwindowsysteminterface.h +++ b/src/gui/kernel/qwindowsysteminterface.h @@ -179,6 +179,9 @@ public: static void handleWindowStateChanged(QWindow *window, Qt::WindowState newState, int oldState = -1); static void handleWindowScreenChanged(QWindow *window, QScreen *newScreen); + template + static void handleSafeAreaMarginsChanged(QWindow *window); + template static void handleApplicationStateChanged(Qt::ApplicationState newState, bool forcePropagate = false); diff --git a/src/gui/kernel/qwindowsysteminterface_p.h b/src/gui/kernel/qwindowsysteminterface_p.h index 0f350fb2d21..5b41ccc3a50 100644 --- a/src/gui/kernel/qwindowsysteminterface_p.h +++ b/src/gui/kernel/qwindowsysteminterface_p.h @@ -98,7 +98,8 @@ public: #endif ApplicationStateChanged = 0x19, FlushEvents = 0x20, - WindowScreenChanged = 0x21 + WindowScreenChanged = 0x21, + SafeAreaMarginsChanged = 0x22 }; class WindowSystemEvent { @@ -187,6 +188,15 @@ public: QPointer screen; }; + class SafeAreaMarginsChangedEvent : public WindowSystemEvent { + public: + SafeAreaMarginsChangedEvent(QWindow *w) + : WindowSystemEvent(SafeAreaMarginsChanged), window(w) + { } + + QPointer window; + }; + class ApplicationStateChangedEvent : public WindowSystemEvent { public: ApplicationStateChangedEvent(Qt::ApplicationState newState, bool forcePropagate = false) diff --git a/src/plugins/platforms/ios/qioswindow.h b/src/plugins/platforms/ios/qioswindow.h index 81fad420f69..85c60f61df6 100644 --- a/src/plugins/platforms/ios/qioswindow.h +++ b/src/plugins/platforms/ios/qioswindow.h @@ -71,6 +71,8 @@ public: bool isExposed() const Q_DECL_OVERRIDE; void propagateSizeHints() Q_DECL_OVERRIDE {} + QMargins safeAreaMargins() const override; + void raise() Q_DECL_OVERRIDE{ raiseOrLower(true); } void lower() Q_DECL_OVERRIDE { raiseOrLower(false); } diff --git a/src/plugins/platforms/ios/qioswindow.mm b/src/plugins/platforms/ios/qioswindow.mm index e934cb90fad..4ce73d7b5d8 100644 --- a/src/plugins/platforms/ios/qioswindow.mm +++ b/src/plugins/platforms/ios/qioswindow.mm @@ -223,6 +223,13 @@ void QIOSWindow::applyGeometry(const QRect &rect) [m_view layoutIfNeeded]; } +QMargins QIOSWindow::safeAreaMargins() const +{ + UIEdgeInsets safeAreaInsets = m_view.qt_safeAreaInsets; + return QMargins(safeAreaInsets.left, safeAreaInsets.top, + safeAreaInsets.right, safeAreaInsets.bottom); +} + bool QIOSWindow::isExposed() const { return qApp->applicationState() != Qt::ApplicationSuspended diff --git a/src/plugins/platforms/ios/quiview.mm b/src/plugins/platforms/ios/quiview.mm index 1dbacad6e73..ed8af8e2900 100644 --- a/src/plugins/platforms/ios/quiview.mm +++ b/src/plugins/platforms/ios/quiview.mm @@ -56,6 +56,24 @@ @implementation QUIView ++ (void)load +{ + if (QOperatingSystemVersion::current() < QOperatingSystemVersion(QOperatingSystemVersion::IOS, 11)) { + // iOS 11 handles this though [UIView safeAreaInsetsDidChange], but there's no signal for + // the corresponding top and bottom layout guides that we use on earlier versions. Note + // that we use the _will_ change version of the notification, because we want to react + // to the change as early was possible. But since the top and bottom layout guides have + // not been updated at this point we use asynchronous delivery of the event, so that the + // event is processed by QtGui just after iOS has updated the layout margins. + [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillChangeStatusBarFrameNotification + object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *) { + for (QWindow *window : QGuiApplication::allWindows()) + QWindowSystemInterface::handleSafeAreaMarginsChanged(window); + } + ]; + } +} + + (Class)layerClass { return [CAEAGLLayer class]; @@ -100,6 +118,22 @@ self.layer.borderColor = colorWithBrightness(1.0); self.layer.borderWidth = 1.0; } + +#if QT_DARWIN_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_NA, 110000, 110000, __WATCHOS_NA) + if (qEnvironmentVariableIsSet("QT_IOS_DEBUG_WINDOW_SAFE_AREAS")) { + if (__builtin_available(iOS 11, tvOS 11, *)) { + UIView *safeAreaOverlay = [[UIView alloc] initWithFrame:CGRectZero]; + [safeAreaOverlay setBackgroundColor:[UIColor colorWithRed:0.3 green:0.7 blue:0.9 alpha:0.3]]; + [self addSubview:safeAreaOverlay]; + + safeAreaOverlay.translatesAutoresizingMaskIntoConstraints = NO; + [safeAreaOverlay.topAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.topAnchor].active = YES; + [safeAreaOverlay.leftAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.leftAnchor].active = YES; + [safeAreaOverlay.rightAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.rightAnchor].active = YES; + [safeAreaOverlay.bottomAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.bottomAnchor].active = YES; + } + } +#endif } return self; @@ -203,6 +237,11 @@ QWindowSystemInterface::handleExposeEvent(m_qioswindow->window(), region); } +- (void)safeAreaInsetsDidChange +{ + QWindowSystemInterface::handleSafeAreaMarginsChanged(m_qioswindow->window()); +} + // ------------------------------------------------------------------------- - (BOOL)canBecomeFirstResponder diff --git a/src/widgets/kernel/qwidget.cpp b/src/widgets/kernel/qwidget.cpp index 125f1cf246a..f279453ebd4 100644 --- a/src/widgets/kernel/qwidget.cpp +++ b/src/widgets/kernel/qwidget.cpp @@ -1198,6 +1198,7 @@ void QWidgetPrivate::init(QWidget *parentWidget, Qt::WindowFlags f) q->setAttribute(Qt::WA_QuitOnClose); // might be cleared in adjustQuitOnCloseAttribute() adjustQuitOnCloseAttribute(); + q->setAttribute(Qt::WA_ContentsMarginsRespectsSafeArea); q->setAttribute(Qt::WA_WState_Hidden); //give potential windows a bigger "pre-initial" size; create_sys() will give them a new size later @@ -1423,8 +1424,6 @@ void QWidgetPrivate::create_sys(WId window, bool initializeWindow, bool destroyO Q_UNUSED(initializeWindow); Q_UNUSED(destroyOldWindow); - Qt::WindowFlags flags = data.window_flags; - if (!q->testAttribute(Qt::WA_NativeWindow) && !q->isWindow()) return; // we only care about real toplevels @@ -1442,12 +1441,19 @@ void QWidgetPrivate::create_sys(WId window, bool initializeWindow, bool destroyO win->setProperty(propertyName, q->property(propertyName)); } + Qt::WindowFlags &flags = data.window_flags; + +#if defined(Q_OS_IOS) || defined(Q_OS_TVOS) + if (q->testAttribute(Qt::WA_ContentsMarginsRespectsSafeArea)) + flags |= Qt::MaximizeUsingFullscreenGeometryHint; +#endif + if (q->testAttribute(Qt::WA_ShowWithoutActivating)) win->setProperty("_q_showWithoutActivating", QVariant(true)); if (q->testAttribute(Qt::WA_MacAlwaysShowToolWindow)) win->setProperty("_q_macAlwaysShowToolWindow", QVariant::fromValue(QVariant(true))); setNetWmWindowTypes(true); // do nothing if none of WA_X11NetWmWindowType* is set - win->setFlags(data.window_flags); + win->setFlags(flags); fixPosIncludesFrame(); if (q->testAttribute(Qt::WA_Moved) || !QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::WindowManagement)) @@ -7588,21 +7594,7 @@ void QWidget::setContentsMargins(int left, int top, int right, int bottom) d->rightmargin = right; d->bottommargin = bottom; - if (QLayout *l=d->layout) - l->update(); //force activate; will do updateGeometry - else - updateGeometry(); - - if (isVisible()) { - update(); - QResizeEvent e(data->crect.size(), data->crect.size()); - QApplication::sendEvent(this, &e); - } else { - setAttribute(Qt::WA_PendingResizeEvent, true); - } - - QEvent e(QEvent::ContentsRectChange); - QApplication::sendEvent(this, &e); + d->updateContentsRect(); } /*! @@ -7627,6 +7619,27 @@ void QWidget::setContentsMargins(const QMargins &margins) margins.right(), margins.bottom()); } +void QWidgetPrivate::updateContentsRect() +{ + Q_Q(QWidget); + + if (layout) + layout->update(); //force activate; will do updateGeometry + else + q->updateGeometry(); + + if (q->isVisible()) { + q->update(); + QResizeEvent e(q->data->crect.size(), q->data->crect.size()); + QApplication::sendEvent(q, &e); + } else { + q->setAttribute(Qt::WA_PendingResizeEvent, true); + } + + QEvent e(QEvent::ContentsRectChange); + QApplication::sendEvent(q, &e); +} + /*! Returns the widget's contents margins for \a left, \a top, \a right, and \a bottom. @@ -7635,15 +7648,22 @@ void QWidget::setContentsMargins(const QMargins &margins) */ void QWidget::getContentsMargins(int *left, int *top, int *right, int *bottom) const { - Q_D(const QWidget); + QMargins m = contentsMargins(); if (left) - *left = d->leftmargin; + *left = m.left(); if (top) - *top = d->topmargin; + *top = m.top(); if (right) - *right = d->rightmargin; + *right = m.right(); if (bottom) - *bottom = d->bottommargin; + *bottom = m.bottom(); +} + +// FIXME: Move to qmargins.h for next minor Qt release +QMargins operator|(const QMargins &m1, const QMargins &m2) +{ + return QMargins(qMax(m1.left(), m2.left()), qMax(m1.top(), m2.top()), + qMax(m1.right(), m2.right()), qMax(m1.bottom(), m2.bottom())); } /*! @@ -7656,10 +7676,11 @@ void QWidget::getContentsMargins(int *left, int *top, int *right, int *bottom) c QMargins QWidget::contentsMargins() const { Q_D(const QWidget); - return QMargins(d->leftmargin, d->topmargin, d->rightmargin, d->bottommargin); + QMargins userMargins(d->leftmargin, d->topmargin, d->rightmargin, d->bottommargin); + return testAttribute(Qt::WA_ContentsMarginsRespectsSafeArea) ? + userMargins | d->safeAreaMargins() : userMargins; } - /*! Returns the area inside the widget's margins. @@ -7667,14 +7688,87 @@ QMargins QWidget::contentsMargins() const */ QRect QWidget::contentsRect() const { - Q_D(const QWidget); - return QRect(QPoint(d->leftmargin, d->topmargin), - QPoint(data->crect.width() - 1 - d->rightmargin, - data->crect.height() - 1 - d->bottommargin)); - + return rect() - contentsMargins(); } +QMargins QWidgetPrivate::safeAreaMargins() const +{ + Q_Q(const QWidget); + QWidget *nativeWidget = q->window(); + if (!nativeWidget->windowHandle()) + return QMargins(); + QPlatformWindow *platformWindow = nativeWidget->windowHandle()->handle(); + if (!platformWindow) + return QMargins(); + + QMargins safeAreaMargins = platformWindow->safeAreaMargins(); + + if (!q->isWindow()) { + // In theory the native parent widget already has a contents rect reflecting + // the safe area of that widget, but we can't be sure that the widget or child + // widgets of that widget have respected the contents rect when setting their + // geometry, so we need to manually compute the safe area. + + // Unless the native widget doesn't have any margins, in which case there's + // nothing for us to compute. + if (safeAreaMargins.isNull()) + return QMargins(); + + // Or, if one of our ancestors are in a layout that does not have WA_LayoutOnEntireRect + // set, then we know that the layout has already taken care of placing us inside the + // safe area, by taking the contents rect of its parent widget into account. + const QWidget *assumedSafeWidget = nullptr; + for (const QWidget *w = q; w != nativeWidget; w = w->parentWidget()) { + QWidget *parentWidget = w->parentWidget(); + if (parentWidget->testAttribute(Qt::WA_LayoutOnEntireRect)) + continue; // Layout not going to help us + + QLayout *layout = parentWidget->layout(); + if (!layout) + continue; + + if (layout->geometry().isNull()) + continue; // Layout hasn't been activated yet + + if (layout->indexOf(const_cast(w)) < 0) + continue; // Widget is not in layout + + assumedSafeWidget = w; + break; + } + +#if !defined(QT_DEBUG) + if (assumedSafeWidget) { + // We found a layout that we assume will take care of keeping us within the safe area + // For debug builds we still map the safe area using the fallback logic, so that we + // can detect any misbehaving layouts. + return QMargins(); + } +#endif + + // In all other cases we need to map the safe area of the native parent to the widget. + // This depends on the widget being positioned and sized already, which means the initial + // layout will be wrong, but the layout will then adjust itself. + QPoint topLeftMargins = q->mapFrom(nativeWidget, QPoint(safeAreaMargins.left(), safeAreaMargins.top())); + QRect widgetRect = q->isVisible() ? q->visibleRegion().boundingRect() : q->rect(); + QPoint bottomRightMargins = widgetRect.bottomRight() - q->mapFrom(nativeWidget, + nativeWidget->rect().bottomRight() - QPoint(safeAreaMargins.right(), safeAreaMargins.bottom())); + + // Margins should never be negative + safeAreaMargins = QMargins(qMax(0, topLeftMargins.x()), qMax(0, topLeftMargins.y()), + qMax(0, bottomRightMargins.x()), qMax(0, bottomRightMargins.y())); + + if (!safeAreaMargins.isNull() && assumedSafeWidget) { + QLayout *layout = assumedSafeWidget->parentWidget()->layout(); + qWarning() << layout << "is laying out" << assumedSafeWidget + << "outside of the contents rect of" << layout->parentWidget(); + return QMargins(); // Return empty margin to visually highlight the error + } + } + + return safeAreaMargins; +} /*! \fn void QWidget::customContextMenuRequested(const QPoint &pos) diff --git a/src/widgets/kernel/qwidget_p.h b/src/widgets/kernel/qwidget_p.h index a24d13e0e12..0c012d19324 100644 --- a/src/widgets/kernel/qwidget_p.h +++ b/src/widgets/kernel/qwidget_p.h @@ -513,6 +513,9 @@ public: void setLayoutItemMargins(int left, int top, int right, int bottom); void setLayoutItemMargins(QStyle::SubElement element, const QStyleOption *opt = 0); + void updateContentsRect(); + QMargins safeAreaMargins() const; + // aboutToDestroy() is called just before the contents of // QWidget::destroy() is executed. It's used to signal QWidget // sub-classes that their internals are about to be released. diff --git a/src/widgets/kernel/qwidgetwindow.cpp b/src/widgets/kernel/qwidgetwindow.cpp index abaebad821c..165dce04e95 100644 --- a/src/widgets/kernel/qwidgetwindow.cpp +++ b/src/widgets/kernel/qwidgetwindow.cpp @@ -87,6 +87,13 @@ public: } QRectF closestAcceptableGeometry(const QRectF &rect) const Q_DECL_OVERRIDE; + + void processSafeAreaMarginsChanged() override + { + Q_Q(QWidgetWindow); + if (QWidget *widget = q->widget()) + QWidgetPrivate::get(widget)->updateContentsRect(); + } }; QRectF QWidgetWindowPrivate::closestAcceptableGeometry(const QRectF &rect) const