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>
This commit is contained in:
Volker Hilsheimer 2021-10-04 21:39:31 +02:00
parent 553a1c48fd
commit 32edae5e26
5 changed files with 169 additions and 50 deletions

View File

@ -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::sizeHint() const
{
return size_helper(&QDockAreaLayoutInfo::sizeHint, &QLayoutItem::sizeHint);
}
QSize QDockAreaLayout::minimumSize() const
{
int left_sep = 0;
int right_sep = 0;
int top_sep = 0;
int bottom_sep = 0;
return size_helper(&QDockAreaLayoutInfo::minimumSize, &QLayoutItem::minimumSize);
}
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;
}
/*!
\internal
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();
if (corners[Qt::BottomLeftCorner] == Qt::LeftDockWidgetArea)
row3 += left.width();
else
col1 += bottom.height();
if (corners[Qt::BottomRightCorner] == Qt::RightDockWidgetArea)
row3 += right.width();
else
col3 += bottom.height();
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

View File

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

View File

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

View File

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

View File

@ -126,6 +126,8 @@ private slots:
void dockWidgetArea();
void restoreState();
void restoreStateFromPreviousVersion();
void restoreStateSizeChanged_data();
void restoreStateSizeChanged();
void createPopupMenu();
void hideBeforeLayout();
#ifdef QT_BUILD_INTERNAL
@ -1391,6 +1393,67 @@ void tst_QMainWindow::restoreStateFromPreviousVersion()
}
void tst_QMainWindow::restoreStateSizeChanged_data()
{
QTest::addColumn<Qt::WindowState>("saveState");
QTest::addColumn<Qt::WindowState>("showState");
QTest::addColumn<bool>("sameSize");
QTest::addRow("fullscreen") << Qt::WindowFullScreen << Qt::WindowFullScreen << true;
QTest::addRow("maximized") << Qt::WindowMaximized << Qt::WindowMaximized << true;
QTest::addRow("maximized->normal") << Qt::WindowMaximized << Qt::WindowNoState << false;
QTest::addRow("fullscreen->normal") << Qt::WindowFullScreen << Qt::WindowNoState << false;
QTest::addRow("fullscreen->maximized") << Qt::WindowFullScreen << Qt::WindowMaximized << false;
QTest::addRow("maximized->fullscreen") << Qt::WindowMaximized << Qt::WindowFullScreen << true;
}
void tst_QMainWindow::restoreStateSizeChanged()
{
QFETCH(Qt::WindowState, saveState);
QFETCH(Qt::WindowState, showState);
QFETCH(bool, sameSize);
auto createMainWindow = []{
QMainWindow *mainWindow = new QMainWindow;
mainWindow->move(QGuiApplication::primaryScreen()->availableGeometry().topLeft());
mainWindow->setCentralWidget(new QLabel("X"));
QDockWidget *dockWidget = new QDockWidget;
dockWidget->setObjectName("Dock Widget");
mainWindow->addDockWidget(Qt::LeftDockWidgetArea, dockWidget);
return mainWindow;
};
QByteArray geometryData;
QByteArray stateData;
int dockWidgetWidth = 0;
QRect normalGeometry;
{
auto mainWindow = QScopedPointer<QMainWindow>(createMainWindow());
mainWindow->setWindowState(saveState);
mainWindow->show();
QVERIFY(QTest::qWaitForWindowExposed(mainWindow.data()));
dockWidgetWidth = mainWindow->width() - 100;
QDockWidget *dockWidget = mainWindow->findChild<QDockWidget*>("Dock Widget");
mainWindow->resizeDocks({dockWidget}, {dockWidgetWidth}, Qt::Horizontal);
geometryData = mainWindow->saveGeometry();
stateData = mainWindow->saveState();
normalGeometry = mainWindow->normalGeometry();
}
auto mainWindow = QScopedPointer<QMainWindow>(createMainWindow());
mainWindow->restoreGeometry(geometryData);
mainWindow->restoreState(stateData);
mainWindow->setWindowState(showState);
mainWindow->show();
QVERIFY(QTest::qWaitForWindowExposed(mainWindow.data()));
QDockWidget *dockWidget = mainWindow->findChild<QDockWidget*>("Dock Widget");
QVERIFY(dockWidget);
QCOMPARE(mainWindow->normalGeometry().size(), normalGeometry.size());
if (sameSize)
QTRY_COMPARE(dockWidget->width(), dockWidgetWidth);
}
void tst_QMainWindow::createPopupMenu()
{
@ -1692,6 +1755,7 @@ void tst_QMainWindow::saveRestore()
adw.apply(&mainWindow);
mainWindow.show();
mainWindow.restoreState(stateData);
COMPARE_DOCK_WIDGET_GEOS(dockWidgetGeos, dockWidgetGeometries(&mainWindow));
@ -1710,6 +1774,7 @@ void tst_QMainWindow::saveRestore()
mainWindow.restoreState(stateData);
mainWindow.show();
QVERIFY(QTest::qWaitForWindowExposed(&mainWindow));
COMPARE_DOCK_WIDGET_GEOS(dockWidgetGeos, dockWidgetGeometries(&mainWindow));
}
}