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 <richard.gustavsen@qt.io>
This commit is contained in:
Tor Arne Vestbø 2017-12-01 19:03:05 +01:00
parent 59febb49e4
commit ba44cdae38
15 changed files with 234 additions and 31 deletions

View File

@ -474,6 +474,8 @@ public:
WA_TabletTracking = 129,
WA_ContentsMarginsRespectsSafeArea = 130,
// Add new attributes before this line
WA_AttributeCount
};

View File

@ -1759,6 +1759,9 @@ void QGuiApplicationPrivate::processWindowSystemEvent(QWindowSystemInterfacePriv
case QWindowSystemInterfacePrivate::WindowScreenChanged:
QGuiApplicationPrivate::processWindowScreenChangedEvent(static_cast<QWindowSystemInterfacePrivate::WindowScreenChangedEvent *>(e));
break;
case QWindowSystemInterfacePrivate::SafeAreaMarginsChanged:
QGuiApplicationPrivate::processSafeAreaMarginsChangedEvent(static_cast<QWindowSystemInterfacePrivate::SafeAreaMarginsChangedEvent *>(e));
break;
case QWindowSystemInterfacePrivate::ApplicationStateChanged: {
QWindowSystemInterfacePrivate::ApplicationStateChangedEvent * changeEvent = static_cast<QWindowSystemInterfacePrivate::ApplicationStateChangedEvent *>(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)

View File

@ -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);

View File

@ -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.

View File

@ -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);

View File

@ -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(); }

View File

@ -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<Delivery>(e);
}
QT_DEFINE_QPA_EVENT_HANDLER(void, handleApplicationStateChanged, Qt::ApplicationState newState, bool forcePropagate)
{
Q_ASSERT(QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::ApplicationState));

View File

@ -179,6 +179,9 @@ public:
static void handleWindowStateChanged(QWindow *window, Qt::WindowState newState, int oldState = -1);
static void handleWindowScreenChanged(QWindow *window, QScreen *newScreen);
template<typename Delivery = QWindowSystemInterface::DefaultDelivery>
static void handleSafeAreaMarginsChanged(QWindow *window);
template<typename Delivery = QWindowSystemInterface::DefaultDelivery>
static void handleApplicationStateChanged(Qt::ApplicationState newState, bool forcePropagate = false);

View File

@ -98,7 +98,8 @@ public:
#endif
ApplicationStateChanged = 0x19,
FlushEvents = 0x20,
WindowScreenChanged = 0x21
WindowScreenChanged = 0x21,
SafeAreaMarginsChanged = 0x22
};
class WindowSystemEvent {
@ -187,6 +188,15 @@ public:
QPointer<QScreen> screen;
};
class SafeAreaMarginsChangedEvent : public WindowSystemEvent {
public:
SafeAreaMarginsChangedEvent(QWindow *w)
: WindowSystemEvent(SafeAreaMarginsChanged), window(w)
{ }
QPointer<QWindow> window;
};
class ApplicationStateChangedEvent : public WindowSystemEvent {
public:
ApplicationStateChangedEvent(Qt::ApplicationState newState, bool forcePropagate = false)

View File

@ -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); }

View File

@ -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

View File

@ -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<QWindowSystemInterface::AsynchronousDelivery>(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

View File

@ -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<QWidget *>(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)

View File

@ -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.

View File

@ -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