Fix QWidget::restoreGeometry when restored geometry is off screen

If a widget's geometry is restored to a screen, which is smaller than
the one it was saved from,
- the widget could appear (partly) off screen
- the widget's title bar and resize handles could be inaccessible

This patch refactors and documents checkRestoredGeometry.
In a first step, the restored geometry's size is checked against
a given screen size. It is corrected if necessary.
In a second step, the restored geometry is moved inside the screen,
if necessary.
It makes the function a static member of QWidgetPrivate in order to
expose it for auto testing and adds a respective test function to
tst_QWidget.

Fixes: QTBUG-77385
Fixes: QTBUG-4397
Task-number: QTBUG-69104
Change-Id: I7172e27bfef86d82cd51de70b40de42e8895bae6
Reviewed-by: Tor Arne Vestbø <tor.arne.vestbo@qt.io>
(cherry picked from commit 5edb71c6d4cb0051d27d023ddcd180c5f59f2725)
Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
This commit is contained in:
Axel Spoerl 2022-12-13 10:43:26 +01:00 committed by Qt Cherry-pick Bot
parent 69c75ebbd8
commit fe4955c2f9
3 changed files with 140 additions and 10 deletions

View File

@ -7382,15 +7382,65 @@ QByteArray QWidget::saveGeometry() const
return array; return array;
} }
static void checkRestoredGeometry(const QRect &availableGeometry, QRect *restoredGeometry, /*!
\internal Check a if \a restoredGeometry fits into \a availableGeometry
This method is used to verify that a widget is restored to a geometry, which
fits into the target screen.
\param frameHeight represents the height of the widget's title bar, which is expected
to be on its top.
If the size of \a restoredGeometry exceeds \a availableGeometry, its height and width
will be resized to be two pixels smaller than \a availableGeometry. An exact match would
be full screen.
If at least one edge of \a restoredGeometry is outside \a availableGeometry,
\a restoredGeometry will be moved
\list
\li down if its top is off screen
\li up if its bottom is off screen
\li right if its left edge is off screen
\li left if its right edge is off screen
\endlist
*/
void QWidgetPrivate::checkRestoredGeometry(const QRect &availableGeometry, QRect *restoredGeometry,
int frameHeight) int frameHeight)
{ {
if (!restoredGeometry->intersects(availableGeometry)) { // compare with restored geometry's height increased by frameHeight
restoredGeometry->moveBottom(qMin(restoredGeometry->bottom(), availableGeometry.bottom())); const int height = restoredGeometry->height() + frameHeight;
restoredGeometry->moveLeft(qMax(restoredGeometry->left(), availableGeometry.left()));
restoredGeometry->moveRight(qMin(restoredGeometry->right(), availableGeometry.right())); // Step 1: Resize if necessary:
// make height / width 2px smaller than screen, because an exact match would be fullscreen
if (availableGeometry.height() <= height)
restoredGeometry->setHeight(availableGeometry.height() - 2 - frameHeight);
if (availableGeometry.width() <= restoredGeometry->width())
restoredGeometry->setWidth(availableGeometry.width() - 2);
// Step 2: Move if necessary:
// Construct a rectangle from restored Geometry adjusted by frameHeight
const QRect restored = restoredGeometry->adjusted(0, -frameHeight, 0, 0);
// Return if restoredGeometry (including frame) fits into screen
if (availableGeometry.contains(restored))
return;
// (size is correct, but at least one edge is off screen)
// Top out of bounds => move down
if (restored.top() <= availableGeometry.top()) {
restoredGeometry->moveTop(availableGeometry.top() + 1 + frameHeight);
} else if (restored.bottom() >= availableGeometry.bottom()) {
// Bottom out of bounds => move up
restoredGeometry->moveBottom(availableGeometry.bottom() - 1);
}
// Left edge out of bounds => move right
if (restored.left() <= availableGeometry.left()) {
restoredGeometry->moveLeft(availableGeometry.left() + 1);
} else if (restored.right() >= availableGeometry.right()) {
// Right edge out of bounds => move left
restoredGeometry->moveRight(availableGeometry.right() - 1);
} }
restoredGeometry->moveTop(qMax(restoredGeometry->top(), availableGeometry.top() + frameHeight));
} }
/*! /*!
@ -7477,7 +7527,9 @@ bool QWidget::restoreGeometry(const QByteArray &geometry)
return false; return false;
} }
const int frameHeight = 20; const int frameHeight = QApplication::style()
? QApplication::style()->pixelMetric(QStyle::PM_TitleBarHeight)
: 20;
if (!restoredNormalGeometry.isValid()) if (!restoredNormalGeometry.isValid())
restoredNormalGeometry = QRect(QPoint(0, frameHeight), sizeHint()); restoredNormalGeometry = QRect(QPoint(0, frameHeight), sizeHint());
@ -7493,11 +7545,11 @@ bool QWidget::restoreGeometry(const QByteArray &geometry)
// Modify the restored geometry if we are about to restore to coordinates // Modify the restored geometry if we are about to restore to coordinates
// that would make the window "lost". This happens if: // that would make the window "lost". This happens if:
// - The restored geometry is completely oustside the available geometry // - The restored geometry is completely or partly oustside the available geometry
// - The title bar is outside the available geometry. // - The title bar is outside the available geometry.
checkRestoredGeometry(availableGeometry, &restoredGeometry, frameHeight); QWidgetPrivate::checkRestoredGeometry(availableGeometry, &restoredGeometry, frameHeight);
checkRestoredGeometry(availableGeometry, &restoredNormalGeometry, frameHeight); QWidgetPrivate::checkRestoredGeometry(availableGeometry, &restoredNormalGeometry, frameHeight);
if (maximized || fullScreen) { if (maximized || fullScreen) {
// set geometry before setting the window state to make // set geometry before setting the window state to make
@ -7529,6 +7581,8 @@ bool QWidget::restoreGeometry(const QByteArray &geometry)
d_func()->topData()->normalGeometry = restoredNormalGeometry; d_func()->topData()->normalGeometry = restoredNormalGeometry;
} else { } else {
setWindowState(windowState() & ~(Qt::WindowMaximized | Qt::WindowFullScreen)); setWindowState(windowState() & ~(Qt::WindowMaximized | Qt::WindowFullScreen));
// FIXME: Why fall back to restoredNormalGeometry if majorVersion <= 2?
if (majorVersion > 2) if (majorVersion > 2)
setGeometry(restoredGeometry); setGeometry(restoredGeometry);
else else

View File

@ -210,6 +210,9 @@ public:
static QWidgetPrivate *get(QWidget *w) { return w->d_func(); } static QWidgetPrivate *get(QWidget *w) { return w->d_func(); }
static const QWidgetPrivate *get(const QWidget *w) { return w->d_func(); } static const QWidgetPrivate *get(const QWidget *w) { return w->d_func(); }
static void checkRestoredGeometry(const QRect &availableGeometry, QRect *restoredGeometry,
int frameHeight);
QWExtra *extraData() const; QWExtra *extraData() const;
QTLWExtra *topData() const; QTLWExtra *topData() const;
QTLWExtra *maybeTopData() const; QTLWExtra *maybeTopData() const;

View File

@ -195,6 +195,8 @@ private slots:
void saveRestoreGeometry(); void saveRestoreGeometry();
void restoreVersion1Geometry_data(); void restoreVersion1Geometry_data();
void restoreVersion1Geometry(); void restoreVersion1Geometry();
void restoreGeometryAfterScreenChange_data();
void restoreGeometryAfterScreenChange();
void widgetAt(); void widgetAt();
#ifdef Q_OS_MACOS #ifdef Q_OS_MACOS
@ -434,6 +436,15 @@ private:
QPointingDevice *m_touchScreen; QPointingDevice *m_touchScreen;
const int m_fuzz; const int m_fuzz;
QPalette simplePalette(); QPalette simplePalette();
private:
enum class ScreenPosition {
OffAbove,
OffLeft,
OffBelow,
OffRight,
Contained
};
}; };
// Testing get/set functions // Testing get/set functions
@ -4245,6 +4256,68 @@ void tst_QWidget::restoreVersion1Geometry()
#endif #endif
} }
void tst_QWidget::restoreGeometryAfterScreenChange_data()
{
QTest::addColumn<ScreenPosition>("screenPosition");
QTest::addColumn<int>("deltaWidth");
QTest::addColumn<int>("deltaHeight");
QTest::addColumn<int>("frameMargin");
QTest::addColumn<bool>("outside");
QTest::newRow("offAboveLarge") << ScreenPosition::OffAbove << 200 << 250 << 20 << true;
QTest::newRow("fitting") << ScreenPosition::Contained << 80 << 80 << 20 << false;
QTest::newRow("offRightWide") << ScreenPosition::OffRight << 150 << 80 << 20 << false;
QTest::newRow("offLeftFitting") << ScreenPosition::OffLeft << 70 << 70 << 20 << true;
QTest::newRow("offBelowHigh") << ScreenPosition::OffBelow << 80 << 200 << 20 << false;
}
void tst_QWidget::restoreGeometryAfterScreenChange()
{
const QList<QScreen *> &screens = QApplication::screens();
QVERIFY2(!screens.isEmpty(), "No screens found.");
const QRect screenGeometry = screens.at(0)->geometry();
QFETCH(ScreenPosition, screenPosition);
QFETCH(int, deltaWidth);
QFETCH(int, deltaHeight);
QFETCH(int, frameMargin);
QFETCH(bool, outside);
QRect restoredGeometry = screenGeometry;
restoredGeometry.setHeight(screenGeometry.height() * deltaHeight / 100);
restoredGeometry.setWidth(screenGeometry.width() * deltaWidth / 100);
const float moveMargin = outside ? 1.2 : 0.75;
switch (screenPosition) {
case ScreenPosition::OffLeft:
restoredGeometry.setLeft(restoredGeometry.width() * (-moveMargin));
break;
case ScreenPosition::OffAbove:
restoredGeometry.setTop(restoredGeometry.height() * (-moveMargin));
break;
case ScreenPosition::OffRight:
restoredGeometry.setRight(restoredGeometry.width() * moveMargin);
break;
case ScreenPosition::OffBelow:
restoredGeometry.setBottom(restoredGeometry.height() * moveMargin);
break;
case ScreenPosition::Contained:
break;
}
// If restored geometry fits into screen and has not been moved,
// it is changed only by frame margin plus one pixel at each edge
const QRect originalGeometry = restoredGeometry.adjusted(1, frameMargin + 1, 1, frameMargin + 1);
QWidgetPrivate::checkRestoredGeometry(screenGeometry, &restoredGeometry, frameMargin);
if (deltaHeight < 100 && deltaWidth < 100 && screenPosition == ScreenPosition::Contained)
QCOMPARE(originalGeometry, restoredGeometry);
// new geometry has to fit on the screen
QVERIFY(screenGeometry.contains(restoredGeometry));
}
void tst_QWidget::widgetAt() void tst_QWidget::widgetAt()
{ {
#ifdef Q_OS_MACOS #ifdef Q_OS_MACOS