QDockWidget: Fix memory leak when closing floating tabs

QDockWidgetGroupWindow (aka "floating tabs") can have a single
QDockWidget child only, when a second QDockWidget is hovering over it.
When the QDockWidgetGroupWindow detects the disappearance of its second
last child, it reparents the last child to the QMainWindow and makes it
floating. It removes its last dock widget child from its own
item_list and itself from its parent's (the main window's) item_list.
Then the QDockWidgetGroupWindow removes itself by calling its
deleteLater() slot.

A removal from an item_list calls the d'tor of QDockAreaLayoutItem,
which deletes the item's subinfo and placeholder item, if they exist.
It does not delete the item's widgetItem. In fact the layout accesses
the widgetItem member to draw placeholders and decorations.
As a consequence, both the QDockWidgetGroupWindowItem and the
QDockWidgetItem are leaked, when the corresponding record is removed.

Implement QDockAreaLayout::takeWidgetItem(), which transfers ownership
of QDockAreaLayoutItem::widgetItem to the caller by returning a
std::unique_ptr.
Call this method in QDockWidgetGroupWindow::destroyOrHideIfEmpty() and
QDockWidgetGroupWindow::reparentToMainWindow().

As a drive-by, use static_cast<QMainWindow *>() and assert a
qobject_cast in debug mode.

Fixes: QTBUG-135442
Pick-to: 6.8 6.5
Change-Id: I055c998f515f2bb461518b7516f56db4673687da
Reviewed-by: Richard Moe Gustavsen <richard.gustavsen@qt.io>
(cherry picked from commit 40766380224b3a38bbf92b72d8d9baa3762bc06a)
Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
This commit is contained in:
Axel Spoerl 2025-04-07 13:58:43 +02:00 committed by Qt Cherry-pick Bot
parent 1ae9914a32
commit 5cafb9c760
3 changed files with 20 additions and 8 deletions

View File

@ -1501,6 +1501,14 @@ QList<int> QDockAreaLayoutInfo::indexOf(const QWidget *widget) const
return QList<int>();
}
std::unique_ptr<QLayoutItem> QDockAreaLayoutInfo::takeWidgetItem(QWidget *widget)
{
std::unique_ptr<QLayoutItem> widgetItem;
if (const auto path = indexOf(widget); !path.isEmpty())
widgetItem.reset(item(path).widgetItem);
return widgetItem;
}
QMainWindowLayout *QDockAreaLayoutInfo::mainWindowLayout() const
{
QMainWindowLayout *result = qt_mainwindow_layout(mainWindow);

View File

@ -145,6 +145,7 @@ public:
QList<int> indexOf(const QWidget *widget) const;
QList<int> indexOfPlaceHolder(const QString &objectName) const;
std::unique_ptr<QLayoutItem> takeWidgetItem(QWidget *widget);
QDockWidget *apply(bool animate);

View File

@ -470,6 +470,12 @@ void QDockWidgetGroupWindow::destroyOrHideIfEmpty()
if (!wasHidden)
dw->show();
}
Q_ASSERT(qobject_cast<QMainWindow *>(parentWidget()));
auto *mainWindow = static_cast<QMainWindow *>(parentWidget());
QMainWindowLayout *mwLayout = qt_mainwindow_layout(mainWindow);
QDockAreaLayoutInfo &parentInfo = mwLayout->layoutState.dockAreaLayout.docks[layoutInfo()->dockPos];
std::unique_ptr<QLayoutItem> cleanup = parentInfo.takeWidgetItem(this);
parentInfo.remove(this);
deleteLater();
}
@ -713,7 +719,8 @@ void QDockWidgetGroupWindow::destroyIfSingleItemLeft()
if (layoutInfo()->indexOf(lastDockWidget).isEmpty())
return;
auto *mainWindow = qobject_cast<QMainWindow *>(parentWidget());
Q_ASSERT(qobject_cast<QMainWindow *>(parentWidget()));
auto *mainWindow = static_cast<QMainWindow *>(parentWidget());
QMainWindowLayout *mwLayout = qt_mainwindow_layout(mainWindow);
// Unplug the last remaining dock widget and hide the group window, to avoid flickering
@ -721,17 +728,12 @@ void QDockWidgetGroupWindow::destroyIfSingleItemLeft()
lastDockWidget->setGeometry(geometry());
hide();
// Get the layout info for the main window dock, where dock widgets need to go
QDockAreaLayoutInfo &parentInfo = mwLayout->layoutState.dockAreaLayout.docks[layoutInfo()->dockPos];
// Re-parent last dock widget
reparentToMainWindow(lastDockWidget);
// the group window could still have placeholder items => clear everything
layoutInfo()->item_list.clear();
// remove the group window and the dock's item_list pointing to it.
parentInfo.remove(this);
destroyOrHideIfEmpty();
}
@ -741,13 +743,14 @@ void QDockWidgetGroupWindow::reparentToMainWindow(QDockWidget *dockWidget)
// - remove it from the floating dock's layout info
// - insert it to the main dock's layout info
// Finally, set draggingDock to nullptr, since the drag is finished.
auto *mainWindow = qobject_cast<QMainWindow *>(parentWidget());
Q_ASSERT(mainWindow);
Q_ASSERT(qobject_cast<QMainWindow *>(parentWidget()));
auto *mainWindow = static_cast<QMainWindow *>(parentWidget());
QMainWindowLayout *mwLayout = qt_mainwindow_layout(mainWindow);
Q_ASSERT(mwLayout);
QDockAreaLayoutInfo &parentInfo = mwLayout->layoutState.dockAreaLayout.docks[layoutInfo()->dockPos];
dockWidget->removeEventFilter(this);
parentInfo.add(dockWidget);
std::unique_ptr<QLayoutItem> cleanup = layoutInfo()->takeWidgetItem(dockWidget);
layoutInfo()->remove(dockWidget);
const bool wasFloating = dockWidget->isFloating();
const bool wasVisible = dockWidget->isVisible();