diff options
author | Volker Hilsheimer <volker.hilsheimer@qt.io> | 2021-10-04 21:39:31 +0200 |
---|---|---|
committer | Volker Hilsheimer <volker.hilsheimer@qt.io> | 2021-10-16 09:46:22 +0200 |
commit | 32edae5e268b968aff82f0713612eff2feffb4e1 (patch) | |
tree | fe97e538232807fdd5e87a0312f7cf94ffa81087 /src/widgets | |
parent | 553a1c48fd7a9078c1a33ad0829eaa84f0afb670 (diff) |
Fix restoring main window state for maximized/fullscreen windows
On systems that asynchronously resize the window to maximized or full
screen state, the window will become visible in its normal geometry
before it gets the final size by the windowing system. This might cause
multiple resize events, to each of which the widget's layout responds
with a call to its setGeometry implementation.
The QMainWindowLayout is special in that it will shrink dock widgets if
there is not enough space for them, but it doesn't grow them back once
there is. With the initial resize event being for a smaller size than
what was restored, the state is not restored correctly, but remains in
the state that fit into the smallest size with which setGeometry got
called.
To fix this, we have to keep the restored state around until the window
either gets a size that is large enough for it to fit, or until we can
be reasonably certain that the windowing system is done resizing the
window while transitioning it to the maximized or full screen state.
Since across the various platforms and windowing systems there is no
reliable way to know when the window reaches its final size, we have
to use a timer that we (re)start for each call to setGeometry with a
size that's not large enough. Once the timer times out, we have to
give up; then the last layout state calculated is the final state.
To calculate the size of the layout, introduce a function to the
QDockAreaLayout that returns the size required for the current sizes
of the docks. Refactor sizeHint and minimumSize (which were identical)
into a helper template that takes member-function pointers to call the
respective method from the dock area layout's content items.
Add a test case for various permutations of the scenario. The timeout
of 150ms is based on running this test case repeatedly on various
desktop platforms and X11 window managers.
Fixes: QTBUG-46620
Change-Id: I489675c2c40d3308ac8194aeb4267172b2fb38be
Reviewed-by: Albert Astals Cid <albert.astals.cid@kdab.com>
Reviewed-by: Lars Knoll <lars.knoll@qt.io>
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
Diffstat (limited to 'src/widgets')
-rw-r--r-- | src/widgets/widgets/qdockarealayout.cpp | 72 | ||||
-rw-r--r-- | src/widgets/widgets/qdockarealayout_p.h | 3 | ||||
-rw-r--r-- | src/widgets/widgets/qmainwindowlayout.cpp | 73 | ||||
-rw-r--r-- | src/widgets/widgets/qmainwindowlayout_p.h | 6 |
4 files changed, 104 insertions, 50 deletions
diff --git a/src/widgets/widgets/qdockarealayout.cpp b/src/widgets/widgets/qdockarealayout.cpp index 5a144224db..626710cd54 100644 --- a/src/widgets/widgets/qdockarealayout.cpp +++ b/src/widgets/widgets/qdockarealayout.cpp @@ -2914,7 +2914,8 @@ void QDockAreaLayout::clear() centralWidgetRect = QRect(); } -QSize QDockAreaLayout::sizeHint() const +template<typename SizePMF, typename CenterPMF> +QSize QDockAreaLayout::size_helper(SizePMF sizeFn, CenterPMF centerFn) const { int left_sep = 0; int right_sep = 0; @@ -2928,11 +2929,12 @@ QSize QDockAreaLayout::sizeHint() const bottom_sep = docks[QInternal::BottomDock].isEmpty() ? 0 : sep; } - QSize left = docks[QInternal::LeftDock].sizeHint() + QSize(left_sep, 0); - QSize right = docks[QInternal::RightDock].sizeHint() + QSize(right_sep, 0); - QSize top = docks[QInternal::TopDock].sizeHint() + QSize(0, top_sep); - QSize bottom = docks[QInternal::BottomDock].sizeHint() + QSize(0, bottom_sep); - QSize center = centralWidgetItem == nullptr ? QSize(0, 0) : centralWidgetItem->sizeHint(); + const QSize left = (docks[QInternal::LeftDock].*sizeFn)() + QSize(left_sep, 0); + const QSize right = (docks[QInternal::RightDock].*sizeFn)() + QSize(right_sep, 0); + const QSize top = (docks[QInternal::TopDock].*sizeFn)() + QSize(0, top_sep); + const QSize bottom = (docks[QInternal::BottomDock].*sizeFn)() + QSize(0, bottom_sep); + const QSize center = centralWidgetItem == nullptr + ? QSize(0, 0) : (centralWidgetItem->*centerFn)(); int row1 = top.width(); int row2 = left.width() + center.width() + right.width(); @@ -2964,54 +2966,24 @@ QSize QDockAreaLayout::sizeHint() const return QSize(qMax(row1, row2, row3), qMax(col1, col2, col3)); } -QSize QDockAreaLayout::minimumSize() const +QSize QDockAreaLayout::sizeHint() const { - int left_sep = 0; - int right_sep = 0; - int top_sep = 0; - int bottom_sep = 0; - - if (centralWidgetItem != nullptr) { - left_sep = docks[QInternal::LeftDock].isEmpty() ? 0 : sep; - right_sep = docks[QInternal::RightDock].isEmpty() ? 0 : sep; - top_sep = docks[QInternal::TopDock].isEmpty() ? 0 : sep; - bottom_sep = docks[QInternal::BottomDock].isEmpty() ? 0 : sep; - } - - QSize left = docks[QInternal::LeftDock].minimumSize() + QSize(left_sep, 0); - QSize right = docks[QInternal::RightDock].minimumSize() + QSize(right_sep, 0); - QSize top = docks[QInternal::TopDock].minimumSize() + QSize(0, top_sep); - QSize bottom = docks[QInternal::BottomDock].minimumSize() + QSize(0, bottom_sep); - QSize center = centralWidgetItem == nullptr ? QSize(0, 0) : centralWidgetItem->minimumSize(); - - int row1 = top.width(); - int row2 = left.width() + center.width() + right.width(); - int row3 = bottom.width(); - int col1 = left.height(); - int col2 = top.height() + center.height() + bottom.height(); - int col3 = right.height(); - - if (corners[Qt::TopLeftCorner] == Qt::LeftDockWidgetArea) - row1 += left.width(); - else - col1 += top.height(); - - if (corners[Qt::TopRightCorner] == Qt::RightDockWidgetArea) - row1 += right.width(); - else - col3 += top.height(); + return size_helper(&QDockAreaLayoutInfo::sizeHint, &QLayoutItem::sizeHint); +} - if (corners[Qt::BottomLeftCorner] == Qt::LeftDockWidgetArea) - row3 += left.width(); - else - col1 += bottom.height(); +QSize QDockAreaLayout::minimumSize() const +{ + return size_helper(&QDockAreaLayoutInfo::minimumSize, &QLayoutItem::minimumSize); +} - if (corners[Qt::BottomRightCorner] == Qt::RightDockWidgetArea) - row3 += right.width(); - else - col3 += bottom.height(); +/*! + \internal - return QSize(qMax(row1, row2, row3), qMax(col1, col2, col3)); + Returns the smallest size that doesn't change the size of any of the dock areas. +*/ +QSize QDockAreaLayout::minimumStableSize() const +{ + return size_helper(&QDockAreaLayoutInfo::size, &QLayoutItem::minimumSize); } /*! \internal diff --git a/src/widgets/widgets/qdockarealayout_p.h b/src/widgets/widgets/qdockarealayout_p.h index 171147c254..57572bcfa1 100644 --- a/src/widgets/widgets/qdockarealayout_p.h +++ b/src/widgets/widgets/qdockarealayout_p.h @@ -271,6 +271,9 @@ public: QSize sizeHint() const; QSize minimumSize() const; + QSize minimumStableSize() const; + template<typename SizePMF, typename CenterPMF> + QSize size_helper(SizePMF sizeFn, CenterPMF centerFn) const; void addDockWidget(QInternal::DockPosition pos, QDockWidget *dockWidget, Qt::Orientation orientation); bool restoreDockWidget(QDockWidget *dockWidget); diff --git a/src/widgets/widgets/qmainwindowlayout.cpp b/src/widgets/widgets/qmainwindowlayout.cpp index 18dd05e63b..60db982cb7 100644 --- a/src/widgets/widgets/qmainwindowlayout.cpp +++ b/src/widgets/widgets/qmainwindowlayout.cpp @@ -675,6 +675,31 @@ QSize QMainWindowLayoutState::minimumSize() const return result; } +/*! + \internal + + Returns whether the layout fits into the main window. +*/ +bool QMainWindowLayoutState::fits() const +{ + Q_ASSERT(mainWindow); + + QSize size; + +#if QT_CONFIG(dockwidget) + size = dockAreaLayout.minimumStableSize(); +#endif + +#if QT_CONFIG(toolbar) + size.rwidth() += toolBarAreaLayout.docks[QInternal::LeftDock].rect.width(); + size.rwidth() += toolBarAreaLayout.docks[QInternal::RightDock].rect.width(); + size.rheight() += toolBarAreaLayout.docks[QInternal::TopDock].rect.height(); + size.rheight() += toolBarAreaLayout.docks[QInternal::BottomDock].rect.height(); +#endif + + return size.width() <= mainWindow->width() && size.height() <= mainWindow->height(); +} + void QMainWindowLayoutState::apply(bool animated) { #if QT_CONFIG(toolbar) @@ -1974,11 +1999,47 @@ void QMainWindowLayout::setGeometry(const QRect &_r) r.setBottom(sbr.top() - 1); } + if (restoredState) { + /* + The main window was hidden and was going to be maximized or full-screened when + the state was restored. The state might have been for a larger window size than + the current size (in _r), and the window might still be in the process of being + shown and transitioning to the final size (there's no reliable way of knowing + this across different platforms). Try again with the restored state. + */ + layoutState = *restoredState; + if (restoredState->fits()) { + restoredState.reset(); + discardRestoredStateTimer.stop(); + } else { + /* + Try again in the next setGeometry call, but discard the restored state + after 150ms without any further tries. That's a reasonably short amount of + time during which we can expect the windowing system to either have completed + showing the window, or resized the window once more (which then restarts the + timer in timerEvent). + If the windowing system is done, then the user won't have had a chance to + change the layout interactively AND trigger another resize. + */ + discardRestoredStateTimer.start(150, this); + } + } + layoutState.rect = r; + layoutState.fitLayout(); applyState(layoutState, false); } +void QMainWindowLayout::timerEvent(QTimerEvent *e) +{ + if (e->timerId() == discardRestoredStateTimer.timerId()) { + discardRestoredStateTimer.stop(); + restoredState.reset(); + } + QLayout::timerEvent(e); +} + void QMainWindowLayout::addItem(QLayoutItem *) { qWarning("QMainWindowLayout::addItem: Please use the public QMainWindow API instead"); } @@ -2781,6 +2842,18 @@ bool QMainWindowLayout::restoreState(QDataStream &stream) if (parentWidget()->isVisible()) { layoutState.fitLayout(); applyState(layoutState, false); + } else { + /* + The state might not fit into the size of the widget as it gets shown, but + if the window is expected to be maximized or full screened, then we might + get several resizes as part of that transition, at the end of which the + state might fit. So keep the restored state around for now and try again + later in setGeometry. + */ + if ((parentWidget()->windowState() & (Qt::WindowFullScreen | Qt::WindowMaximized)) + && !layoutState.fits()) { + restoredState.reset(new QMainWindowLayoutState(layoutState)); + } } savedState.deleteAllLayoutItems(); diff --git a/src/widgets/widgets/qmainwindowlayout_p.h b/src/widgets/widgets/qmainwindowlayout_p.h index 20518bc2c9..55385cc164 100644 --- a/src/widgets/widgets/qmainwindowlayout_p.h +++ b/src/widgets/widgets/qmainwindowlayout_p.h @@ -414,6 +414,7 @@ public: QSize sizeHint() const; QSize minimumSize() const; + bool fits() const; void fitLayout(); QLayoutItem *itemAt(int index, int *x) const; @@ -451,6 +452,7 @@ class Q_AUTOTEST_EXPORT QMainWindowLayout public: QMainWindowLayoutState layoutState, savedState; + std::unique_ptr<QMainWindowLayoutState> restoredState; QMainWindowLayout(QMainWindow *mainwindow, QLayout *parentLayout); ~QMainWindowLayout(); @@ -546,6 +548,7 @@ public: }; void saveState(QDataStream &stream) const; bool restoreState(QDataStream &stream); + QBasicTimer discardRestoredStateTimer; // QLayout interface @@ -584,6 +587,9 @@ public: void restore(bool keepSavedState = false); void animationFinished(QWidget *widget); +protected: + void timerEvent(QTimerEvent *e) override; + private Q_SLOTS: void updateGapIndicator(); #if QT_CONFIG(dockwidget) |