Fix crash when grouping items in the layout designer#65742
Fix crash when grouping items in the layout designer#65742vicquick wants to merge 1 commit intoqgis:masterfrom
Conversation
|
Why did you delete the AI use disclaimer? |
|
Can you do a build with ENABLE_MODELTEST=TRUE, and then try to reproduce the bug with your fix included? That will flag if it introduces any new issues. |
Hi, please excuse my oversight! That was unintentional, I indeed used AI to create this PR. I now updated the PR body with the disclaimer. |
|
Built at Ran No model tester assertions. |
…te rows QgsLayoutItemsListView::updateSelection() was connected to QItemSelectionModel::selectionChanged with a direct connection. When the source QgsLayoutModel begins a row insertion (for example when creating a new group via QgsLayoutView::groupSelectedItems -> addLayoutItem -> rebuildZList -> beginInsertRows) and there are persistent indices held by the items list view's selection model, those indices shift and QItemSelectionModel emits selectionChanged synchronously, from inside the still-open beginInsertRows transaction. updateSelection() then runs while the source model is mid-transaction and calls setSelected() on layout items, which emits dataChanged() on the source model. Emitting dataChanged from inside a beginInsertRows/endInsertRows bracket is a QAbstractItemModel contract violation. The observable consequences differed by Qt version: - Qt 6.8: QTreeView::dataChanged eagerly touches the selection model, which re-emits selectionChanged, re-enters updateSelection(), and the cycle recurses until the stack overflows. - Qt 6.9+: The mid-transaction dataChanged corrupts QSortFilterProxyModel's internal source-to-proxy mapping cache. The newly inserted row ends up with two proxy rows mapping to the same source row, so the Items list panel visibly shows a duplicate entry for the newly created group. Closing and reopening the layout rebuilds the proxy from scratch and the duplicate disappears. Both symptoms share the same root cause: updateSelection() running synchronously inside an open source-model transaction. Making the selectionChanged -> updateSelection connection a Qt::QueuedConnection defers the slot to the next event loop iteration, by which time endInsertRows() has fired and the proxy is in a consistent state. The mUpdatingSelection re-entry guard is also added to updateSelection()'s early return, matching the guard that onItemFocused() already uses. With Qt::QueuedConnection this is defense in depth, but it is cheap and protects against future refactors that might reintroduce a synchronous path into the slot. Fixes qgis#61702 Assisted-by: Claude (Anthropic)
e63df2b to
51a789b
Compare
QgsMapLayerStyleManagerWidget::styleClicked() was connected to QAbstractItemView::clicked with a direct connection. When the user clicks a saved style in the Layer Styling panel's Style Manager tab, styleClicked() calls setCurrentStyle() synchronously from inside QListView::mouseReleaseEvent(). setCurrentStyle() emits currentStyleChanged(), which triggers currentStyleChanged() → mStyleList->setCurrentIndex(). This modifies the view's selection model while mouseReleaseEvent still holds a QPersistentModelIndex (d->pressedIndex). On Qt 6, the invalidated persistent index causes a use-after-free when mouseReleaseEvent continues after the clicked() handler returns. On Windows: access violation (0xbaadf00d = freed heap). On Linux: SIGSEGV → SIGABRT. The crash does NOT happen when switching styles via: - Right-click layer → Styles submenu (QAction::triggered fires from the menu event loop, outside any view event handler) - Layer Properties dialog (QComboBox::currentIndexChanged, also outside a view event handler) Fix: change the clicked → styleClicked connection to Qt::QueuedConnection, so styleClicked() runs on the next event loop iteration — after mouseReleaseEvent has fully completed and released d->pressedIndex. Same pattern as the layout group crash fixed in qgis#65742. Fixes qgis#65476 Assisted-by: Claude (Anthropic)
styleClicked() was connected to QAbstractItemView::clicked with a direct connection. When the user clicks a saved style in the Layer Styling panel's Style Manager tab, styleClicked() calls setCurrentStyle() synchronously from inside QListView::mouseReleaseEvent(). setCurrentStyle() emits currentStyleChanged(), which triggers currentStyleChanged() → mStyleList->setCurrentIndex(). This modifies the view's selection model while mouseReleaseEvent still holds a QPersistentModelIndex (d->pressedIndex). On Qt 6, the invalidated persistent index causes a use-after-free when mouseReleaseEvent continues after the clicked() handler returns. On Windows: access violation (0xbaadf00d = freed heap). On Linux: SIGSEGV → SIGABRT. The crash does NOT happen when switching styles via: - Right-click layer → Styles submenu (QAction::triggered fires from the menu event loop, outside any view event handler) - Layer Properties dialog (QComboBox::currentIndexChanged, also outside a view event handler) Fix: change the clicked → styleClicked connection to Qt::QueuedConnection, so styleClicked() runs on the next event loop iteration — after mouseReleaseEvent has fully completed and released d->pressedIndex. Same pattern as the layout group crash fixed in qgis#65742. Fixes qgis#65476 Assisted-by: Claude (Anthropic)
|
@vicquick testqgslayoutmodel.cpp doesn't use the model test. You need to manually trigger the test by opening a layout designer window, and then grouping items. (Ie -- you can't rely on AI to do that test for you) |
Understood, the automated tests do not cover that. Whilst I manually tested before, I have now rebuilt with -DENABLE_MODELTEST=TRUE, opened a layout, added several items, grouped/ungrouped several times — no crash, and no model warnings appeared, the logs stayed clean. On master without the fix, however, grouping crashes immediately with a stack overflow. |
|
Thanks for the confirmation @vicquick ! |
styleClicked() was connected to QAbstractItemView::clicked with a direct connection. When the user clicks a saved style in the Layer Styling panel's Style Manager tab, styleClicked() calls setCurrentStyle() synchronously from inside QListView::mouseReleaseEvent(). setCurrentStyle() emits currentStyleChanged(), which triggers currentStyleChanged() → mStyleList->setCurrentIndex(). This modifies the view's selection model while mouseReleaseEvent still holds a QPersistentModelIndex (d->pressedIndex). On Qt 6, the invalidated persistent index causes a use-after-free when mouseReleaseEvent continues after the clicked() handler returns. On Windows: access violation (0xbaadf00d = freed heap). On Linux: SIGSEGV → SIGABRT. The crash does NOT happen when switching styles via: - Right-click layer → Styles submenu (QAction::triggered fires from the menu event loop, outside any view event handler) - Layer Properties dialog (QComboBox::currentIndexChanged, also outside a view event handler) Fix: change the clicked → styleClicked connection to Qt::QueuedConnection, so styleClicked() runs on the next event loop iteration — after mouseReleaseEvent has fully completed and released d->pressedIndex. Same pattern as the layout group crash fixed in qgis#65742. Fixes qgis#65476 Assisted-by: Claude (Anthropic)
🪟 Windows Qt6 buildsDownload Windows Qt6 builds of this PR for testing. 🍎 MacOS Qt6 buildsDownload MacOS Qt6 builds of this PR for testing. |
styleClicked() was connected to QAbstractItemView::clicked with a direct connection. When the user clicks a saved style in the Layer Styling panel's Style Manager tab, styleClicked() calls setCurrentStyle() synchronously from inside QListView::mouseReleaseEvent(). setCurrentStyle() emits currentStyleChanged(), which triggers currentStyleChanged() → mStyleList->setCurrentIndex(). This modifies the view's selection model while mouseReleaseEvent still holds a QPersistentModelIndex (d->pressedIndex). On Qt 6, the invalidated persistent index causes a use-after-free when mouseReleaseEvent continues after the clicked() handler returns. On Windows: access violation (0xbaadf00d = freed heap). On Linux: SIGSEGV → SIGABRT. The crash does NOT happen when switching styles via: - Right-click layer → Styles submenu (QAction::triggered fires from the menu event loop, outside any view event handler) - Layer Properties dialog (QComboBox::currentIndexChanged, also outside a view event handler) Fix: change the clicked → styleClicked connection to Qt::QueuedConnection, so styleClicked() runs on the next event loop iteration — after mouseReleaseEvent has fully completed and released d->pressedIndex. Same pattern as the layout group crash fixed in qgis#65742. Fixes qgis#65476 Assisted-by: Claude (Anthropic)
|
The QGIS project highly values your contribution and would love to see this work merged! Unfortunately this PR has been marked as AI written and has not had any activity in the last 5 days and is being automatically marked as "stale". If you think this pull request should be merged, please check
|
Description
Fixes #61702
Reproduction
On master at
ce4915f3:Root cause
QgsLayoutItemsListView::updateSelection()is connected toQItemSelectionModel::selectionChangedwith a direct connection.When a new group is created,
QgsLayoutView::groupSelectedItems→QgsLayout::addLayoutItem→QgsLayoutModel::rebuildZListcallsbeginInsertRows, which shifts the persistent indices held by the items list view's selection model and causesQItemSelectionModelto emitselectionChangedsynchronously — from inside the still-openbeginInsertRowstransaction.updateSelection()then runs mid-transaction and callsQgsLayoutItem::setSelected()on items, which callsQgsLayoutModel::updateItemSelectStatus()and emitsdataChangedon the source model. EmittingdataChangedfrom inside abeginInsertRows/endInsertRowsbracket is aQAbstractItemModelcontract violation.On Qt 6.9.2 this cascades into infinite recursion:
The earlier manifestation reported in the issue thread — a duplicate row appearing for the newly created group in the items list that disappears after closing and reopening the layout — is the same root cause: the mid-transaction
dataChangedcorruptsQSortFilterProxyModel's internal source-to-proxy mapping cache and the new row ends up mapped twice. Rebuilding the proxy from scratch (by closing and reopening the layout) clears the duplicate.Fix
Change the
selectionChanged→updateSelectionconnection toQt::QueuedConnection, soupdateSelection()runs on the next event loop iteration — afterendInsertRowshas completed and the source model is in a consistent state.Also add
mUpdatingSelectiontoupdateSelection()'s re-entry guard, matching the guard thatonItemFocused()already uses. With the queued connection this is defense in depth, but it is cheap and protects against future refactors that might reintroduce a synchronous path into the slot.Total diff: 1 file, +6 −3 lines.
Verification
Built master at
ce4915f3twice, unpatched and with this patch, on Ubuntu 25.10 + Qt 6.9.2:AI tool usage
Claude assisted with root cause analysis, fix implementation, and PR description drafting. The fix was verified by building QGIS from source and testing manually. I have reviewed and understand all code in this PR.