Skip to content

Fix layout items list selection update race causing crash and duplica… - #355

Closed
agiudiceandrea wants to merge 99 commits into
agiudiceandrea:masterfrom
vicquick:fix/61702-layout-items-list-queued-connection
Closed

Fix layout items list selection update race causing crash and duplica…#355
agiudiceandrea wants to merge 99 commits into
agiudiceandrea:masterfrom
vicquick:fix/61702-layout-items-list-queued-connection

Conversation

@agiudiceandrea

@agiudiceandrea agiudiceandrea commented Aug 25, 2026

Copy link
Copy Markdown
Owner

…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.

Assisted-by: Claude (Anthropic)

Description

[Replace this with some text explaining the rationale and details about this pull request]

AI tool usage

  • AI tool(s) (Copilot, Claude, or something similar) supported my development of this PR. See our policy about AI tool use. Use of AI tools must be indicated. Failure to be honest might result in banning.

nyalldawson and others added 30 commits August 17, 2026 09:27
Returns an expression which is the simplest direct equivalent of this expression

Eg. an expression like ``1 + 2`` can be effectively replaced by the expression ``3``,
or ``(1 + 2) * "a"`` by ``3 * a``.
* install built app
* add to debian packaging
* superceeds qgis#67064
nyalldawson and others added 9 commits August 26, 2026 05:57
This was previously done via private classes in app, but those
are inaccessible to gui widgets or python plugins.
...instead of relying on iface methods. Removes another roadblock
from porting this code to c++
Don't write default rotation properties to xml
@github-actions

Copy link
Copy Markdown

🪟 Windows Qt6 builds

Download Windows Qt6 builds of this PR for testing.
To execute locally, unzip the downloaded zip file and run bin\qgis-bin.exe in the extracted directory.
You might be prompted by Windows Defender click "Run anyway"
(Built from commit 51a789b)

…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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.