From 702e7e31739591490d49d8a8d00f68c411f5d51d Mon Sep 17 00:00:00 2001 From: Magne Sjaastad Date: Mon, 10 Aug 2026 15:05:07 +0200 Subject: [PATCH] #14505 caf: Add cafTestApplication repro for reentrant property editor rebuild Adds a ReentrantEditorRebuild demo object that rebuilds all property editors from inside fieldChangedByUi(), while Qt is still executing QLineEdit::focusOutEvent. Type a value into Realization Filter and press Tab to trigger. The change handler drops both the line edit field and its parent group from the ui ordering, so PdmUiFormLayoutObjectEditor::configureAndUpdateUi() tears down widgets that are still in use further up the stack. Uncheck Rebuild Layout On Change to run the same edit without the reentrant rebuild. --- .../cafTestApplication/CMakeLists.txt | 2 + .../cafTestApplication/MainWindow.cpp | 2 + .../PdmObjects/ReentrantEditorRebuild.cpp | 89 +++++++++++++++++++ .../PdmObjects/ReentrantEditorRebuild.h | 40 +++++++++ 4 files changed, 133 insertions(+) create mode 100644 Fwk/AppFwk/cafTests/cafTestApplication/PdmObjects/ReentrantEditorRebuild.cpp create mode 100644 Fwk/AppFwk/cafTests/cafTestApplication/PdmObjects/ReentrantEditorRebuild.h diff --git a/Fwk/AppFwk/cafTests/cafTestApplication/CMakeLists.txt b/Fwk/AppFwk/cafTests/cafTestApplication/CMakeLists.txt index dff9c575ff..28a91ff43e 100644 --- a/Fwk/AppFwk/cafTests/cafTestApplication/CMakeLists.txt +++ b/Fwk/AppFwk/cafTests/cafTestApplication/CMakeLists.txt @@ -64,6 +64,8 @@ set(PROJECT_FILES PdmObjects/OptionalFields.cpp PdmObjects/ValidationTest.h PdmObjects/ValidationTest.cpp + PdmObjects/ReentrantEditorRebuild.h + PdmObjects/ReentrantEditorRebuild.cpp ) qt_add_executable( diff --git a/Fwk/AppFwk/cafTests/cafTestApplication/MainWindow.cpp b/Fwk/AppFwk/cafTests/cafTestApplication/MainWindow.cpp index 8dd7246c85..1af0a77f9b 100644 --- a/Fwk/AppFwk/cafTests/cafTestApplication/MainWindow.cpp +++ b/Fwk/AppFwk/cafTests/cafTestApplication/MainWindow.cpp @@ -13,6 +13,7 @@ #include "PdmObjects/LineEditAndPushButtons.h" #include "PdmObjects/ManyGroups.h" #include "PdmObjects/OptionalFields.h" +#include "PdmObjects/ReentrantEditorRebuild.h" #include "PdmObjects/SingleEditorPdmObject.h" #include "PdmObjects/SmallDemoPdmObject.h" #include "PdmObjects/SmallDemoPdmObjectA.h" @@ -196,6 +197,7 @@ void MainWindow::buildTestModel() m_testRoot->objects.push_back( new LineEditAndPushButtons ); m_testRoot->objects.push_back( new LabelsAndHyperlinks ); m_testRoot->objects.push_back( new OptionalFields ); + m_testRoot->objects.push_back( new ReentrantEditorRebuild ); auto tamComboBox = new TamComboBox; m_testRoot->objects.push_back( tamComboBox ); diff --git a/Fwk/AppFwk/cafTests/cafTestApplication/PdmObjects/ReentrantEditorRebuild.cpp b/Fwk/AppFwk/cafTests/cafTestApplication/PdmObjects/ReentrantEditorRebuild.cpp new file mode 100644 index 0000000000..2c63f8eae1 --- /dev/null +++ b/Fwk/AppFwk/cafTests/cafTestApplication/PdmObjects/ReentrantEditorRebuild.cpp @@ -0,0 +1,89 @@ +#include "ReentrantEditorRebuild.h" + +#include "cafPdmUiLineEditor.h" +#include "cafPdmUiOrdering.h" + +CAF_PDM_SOURCE_INIT( ReentrantEditorRebuild, "ReentrantEditorRebuild" ); + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +ReentrantEditorRebuild::ReentrantEditorRebuild() + : m_hideFilterGroup( false ) +{ + CAF_PDM_InitObject( "Reentrant Editor Rebuild", "", "", "" ); + + CAF_PDM_InitField( &m_realizationFilter, + "RealizationFilter", + QString(), + "Realization Filter", + "", + "Type a value here and press Tab", + "" ); + + CAF_PDM_InitField( &m_statusField, "Status", QString( "Type into Realization Filter, then press Tab" ), "Status", "", "", "" ); + m_statusField.uiCapability()->setUiReadOnly( true ); + + CAF_PDM_InitField( &m_rebuildLayoutOnChange, + "RebuildLayoutOnChange", + true, + "Rebuild Layout On Change", + "", + "Rebuild all property editors from inside fieldChangedByUi()", + "" ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void ReentrantEditorRebuild::fieldChangedByUi( const caf::PdmFieldHandle* changedField, + const QVariant& oldValue, + const QVariant& newValue ) +{ + if ( changedField == &m_realizationFilter ) + { + m_statusField = "Committed value: " + m_realizationFilter(); + + if ( m_rebuildLayoutOnChange ) + { + // Drop the group that parents the line edit, and rebuild every property editor while + // still inside QLineEdit::focusOutEvent. This mirrors + // RimSumoDataSource::fieldChangedByUi() -> RimSummaryEnsembleSumo::onRealizationSelectionChanged(). + m_hideFilterGroup = true; + + updateAllRequiredEditors(); + } + } +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void ReentrantEditorRebuild::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) +{ + uiOrdering.add( &m_statusField ); + uiOrdering.add( &m_rebuildLayoutOnChange ); + + if ( !m_hideFilterGroup ) + { + // The line edit lives inside this group. When both the field and the group disappear from + // the ui ordering, PdmUiFormLayoutObjectEditor::configureAndUpdateUi() first deletes the + // field editor, whose destructor only calls deleteLater() on the QLineEdit, and then + // destroys the parent QMinimizePanel with a raw delete. ~QWidget destroys its children + // immediately, so the QLineEdit is gone before the pending deleteLater() can run. + auto group = uiOrdering.addNewGroup( "Filter" ); + group->add( &m_realizationFilter ); + } + + uiOrdering.addNewButton( "Restore Filter Group", [this]() { restoreFilterGroup(); } ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void ReentrantEditorRebuild::restoreFilterGroup() +{ + m_hideFilterGroup = false; + + updateAllRequiredEditors(); +} diff --git a/Fwk/AppFwk/cafTests/cafTestApplication/PdmObjects/ReentrantEditorRebuild.h b/Fwk/AppFwk/cafTests/cafTestApplication/PdmObjects/ReentrantEditorRebuild.h new file mode 100644 index 0000000000..95ffb55037 --- /dev/null +++ b/Fwk/AppFwk/cafTests/cafTestApplication/PdmObjects/ReentrantEditorRebuild.h @@ -0,0 +1,40 @@ +#pragma once + +#include "cafPdmField.h" +#include "cafPdmObject.h" + +//================================================================================================== +/// Reproduction of https://github.com/OPM/ResInsight/issues/14505 +/// +/// Rebuilding the property editors from inside fieldChangedByUi() destroys widgets that Qt is +/// still executing an event handler on, giving a use-after-free in QLineEdit::focusOutEvent. +/// +/// Steps to reproduce: +/// 1. Select this object in the tree view so the property editor is shown. +/// 2. Type a value into "Realization Filter". +/// 3. Press Tab. Do not press Enter, the focus-out path is the dangerous one. Enter is handled +/// by PdmUiLineEditor::eventFilter, which calls slotEditingFinished() outside focusOutEvent. +/// +/// Uncheck "Rebuild Layout On Change" to get the same edit without the reentrant rebuild. +//================================================================================================== +class ReentrantEditorRebuild : public caf::PdmObject +{ + CAF_PDM_HEADER_INIT; + +public: + ReentrantEditorRebuild(); + + void fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue ) override; + +private: + void defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) override; + + void restoreFilterGroup(); + +private: + caf::PdmField m_realizationFilter; + caf::PdmField m_statusField; + caf::PdmField m_rebuildLayoutOnChange; + + bool m_hideFilterGroup; +};