diff options
author | Axel Spoerl <axel.spoerl@qt.io> | 2022-12-13 10:43:26 +0100 |
---|---|---|
committer | Axel Spoerl <axel.spoerl@qt.io> | 2022-12-16 22:58:11 +0100 |
commit | 5edb71c6d4cb0051d27d023ddcd180c5f59f2725 (patch) | |
tree | 0da5eda94d2e30808e3945e067237b6351b4cde5 | |
parent | 4ccb10dc918c3d4d01afa1ad718a24eb5fa49a3c (diff) |
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
Pick-to: 6.5 6.4
Change-Id: I7172e27bfef86d82cd51de70b40de42e8895bae6
Reviewed-by: Tor Arne Vestbø <tor.arne.vestbo@qt.io>
-rw-r--r-- | src/widgets/kernel/qwidget.cpp | 74 | ||||
-rw-r--r-- | src/widgets/kernel/qwidget_p.h | 3 | ||||
-rw-r--r-- | tests/auto/widgets/kernel/qwidget/tst_qwidget.cpp | 73 |
3 files changed, 140 insertions, 10 deletions
diff --git a/src/widgets/kernel/qwidget.cpp b/src/widgets/kernel/qwidget.cpp index 0b3333a718..3ad88957ba 100644 --- a/src/widgets/kernel/qwidget.cpp +++ b/src/widgets/kernel/qwidget.cpp @@ -7382,15 +7382,65 @@ QByteArray QWidget::saveGeometry() const 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) { - if (!restoredGeometry->intersects(availableGeometry)) { - restoredGeometry->moveBottom(qMin(restoredGeometry->bottom(), availableGeometry.bottom())); - restoredGeometry->moveLeft(qMax(restoredGeometry->left(), availableGeometry.left())); - restoredGeometry->moveRight(qMin(restoredGeometry->right(), availableGeometry.right())); + // compare with restored geometry's height increased by frameHeight + const int height = restoredGeometry->height() + frameHeight; + + // 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; } - const int frameHeight = 20; + const int frameHeight = QApplication::style() + ? QApplication::style()->pixelMetric(QStyle::PM_TitleBarHeight) + : 20; if (!restoredNormalGeometry.isValid()) 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 // 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. - checkRestoredGeometry(availableGeometry, &restoredGeometry, frameHeight); - checkRestoredGeometry(availableGeometry, &restoredNormalGeometry, frameHeight); + QWidgetPrivate::checkRestoredGeometry(availableGeometry, &restoredGeometry, frameHeight); + QWidgetPrivate::checkRestoredGeometry(availableGeometry, &restoredNormalGeometry, frameHeight); if (maximized || fullScreen) { // set geometry before setting the window state to make @@ -7529,6 +7581,8 @@ bool QWidget::restoreGeometry(const QByteArray &geometry) d_func()->topData()->normalGeometry = restoredNormalGeometry; } else { setWindowState(windowState() & ~(Qt::WindowMaximized | Qt::WindowFullScreen)); + + // FIXME: Why fall back to restoredNormalGeometry if majorVersion <= 2? if (majorVersion > 2) setGeometry(restoredGeometry); else diff --git a/src/widgets/kernel/qwidget_p.h b/src/widgets/kernel/qwidget_p.h index 25a9c75701..d9d9825b6f 100644 --- a/src/widgets/kernel/qwidget_p.h +++ b/src/widgets/kernel/qwidget_p.h @@ -210,6 +210,9 @@ public: static QWidgetPrivate *get(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; QTLWExtra *topData() const; QTLWExtra *maybeTopData() const; diff --git a/tests/auto/widgets/kernel/qwidget/tst_qwidget.cpp b/tests/auto/widgets/kernel/qwidget/tst_qwidget.cpp index 0b495b2379..7e7cc8da7c 100644 --- a/tests/auto/widgets/kernel/qwidget/tst_qwidget.cpp +++ b/tests/auto/widgets/kernel/qwidget/tst_qwidget.cpp @@ -195,6 +195,8 @@ private slots: void saveRestoreGeometry(); void restoreVersion1Geometry_data(); void restoreVersion1Geometry(); + void restoreGeometryAfterScreenChange_data(); + void restoreGeometryAfterScreenChange(); void widgetAt(); #ifdef Q_OS_MACOS @@ -434,6 +436,15 @@ private: QPointingDevice *m_touchScreen; const int m_fuzz; QPalette simplePalette(); + +private: + enum class ScreenPosition { + OffAbove, + OffLeft, + OffBelow, + OffRight, + Contained + }; }; // Testing get/set functions @@ -4245,6 +4256,68 @@ void tst_QWidget::restoreVersion1Geometry() #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() { #ifdef Q_OS_MACOS |