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:
parent
553a1c48fd
commit
32edae5e26
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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)
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user