#13987 PDM: Add caf::PdmNestedCollection framework

Introduce a shared base for tree-shaped folder containers that hold a
list of leaf items together with a list of subcontainers of the same
kind. Replaces the per-class plumbing (subcollections field, top-folder
flag, add/remove/find sub-collection, factory) that is duplicated across
RimSurfaceCollection, RimPolygonCollection and similar candidates.

- caf::PdmNestedCollectionBase: non-templated PdmObject base exposing
  collection-name, add-new-subcollection, leaf-check and topmost-folder
  marking for generic command features and script methods.
- caf::PdmNestedCollection<SelfT, ItemT>: CRTP template that owns the
  typed m_subCollections field and inherits the m_items array from
  PdmObjectCollection<ItemT>. Provides allItems(), find-sub-collection,
  and a typed factory hook.
- RimNestedMirrorCollectionInView: view-side analogue for in-view
  collections that mirror a nested source collection, with orphan-sweep
  + find-or-create + reorder sync.

Design rationale (templated vs non-templated, break-even on derived
class count) lives in docs/pdm-nested-collection-design.md.

No callers migrated in this commit.
This commit is contained in:
Magne Sjaastad committed 2026-05-13 14:53:00 +02:00
1 parent 4055954144
commit 92da6633bb
9 files changed
+934

No files matched your search

@@ -71,6 +71,8 @@ set(SOURCE_GROUP_HEADER_FILES
${CMAKE_CURRENT_LIST_DIR}/RimNamedObject.h
${CMAKE_CURRENT_LIST_DIR}/RimCheckableNamedObject.h
${CMAKE_CURRENT_LIST_DIR}/RimCheckableObject.h
${CMAKE_CURRENT_LIST_DIR}/RimNestedMirrorCollectionInView.h
${CMAKE_CURRENT_LIST_DIR}/RimNestedMirrorCollectionInView.inl
${CMAKE_CURRENT_LIST_DIR}/RimGridTimeHistoryCurve.h
${CMAKE_CURRENT_LIST_DIR}/RimEclipseGeometrySelectionItem.h
${CMAKE_CURRENT_LIST_DIR}/RimDialogData.h
@@ -0,0 +1,84 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#pragma once
#include "RimCheckableNamedObject.h"
#include "cafPdmChildArrayField.h"
#include "cafPdmPtrField.h"
#include <vector>
//==================================================================================================
///
/// View-side analogue of caf::PdmNestedCollection.
///
/// Generic CRTP template for in-view collections that mirror a nested source collection
/// (a caf::PdmNestedCollection-derived type). Provides a flat items vector, a vector of
/// sub-collections of the same (derived) type, a source-collection pointer, and a sync
/// engine that mirrors the source tree by orphan-sweeping, find-or-creating wrappers,
/// and reordering to match the source.
///
/// Template parameters:
/// SelfT - The derived class (CRTP).
/// SourceT - The source collection type. Must expose collectionName(), and the data
/// needed by the sourceSubCollections()/sourceItems() hooks.
/// ItemViewT - View-side item wrapper type. Must declare a SourceItemT typedef and a
/// sourceItem() accessor returning SourceItemT*.
///
/// Derived classes must call CAF_PDM_InitFieldNoDefault for the inherited fields
/// m_itemsInView, m_collectionsInView and m_sourceCollection, so that the XML keywords
/// stay derived-class-specific.
//==================================================================================================
template <typename SelfT, typename SourceT, typename ItemViewT>
class RimNestedMirrorCollectionInView : public RimCheckableNamedObject
{
public:
using SourceItemT = typename ItemViewT::SourceItemT;
SourceT* sourceCollection() const;
void setSourceCollection( SourceT* source );
// Public sync entry. Refreshes the view tree from the current source.
void updateFromSource();
protected:
RimNestedMirrorCollectionInView();
~RimNestedMirrorCollectionInView() override;
// Pure virtual hooks - derived class adapts to its source type.
virtual std::vector<SourceT*> sourceSubCollections() const = 0;
virtual std::vector<SourceItemT*> sourceItems() const = 0;
virtual ItemViewT* createItemInView( SourceItemT* src ) = 0;
// Default impl: new SelfT() + setSourceCollection(src). Override for extra wiring.
virtual SelfT* createSubCollectionInView( SourceT* src );
// Recursive sync: sub-collections, then items, then editor refresh.
void updateAllViewItems();
SelfT* findCollectionInViewForSource( const SourceT* src ) const;
ItemViewT* findItemInViewForSource( const SourceItemT* src ) const;
caf::PdmChildArrayField<ItemViewT*> m_itemsInView;
caf::PdmChildArrayField<SelfT*> m_collectionsInView;
caf::PdmPtrField<SourceT*> m_sourceCollection;
};
#include "RimNestedMirrorCollectionInView.inl"
@@ -0,0 +1,192 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
template <typename SelfT, typename SourceT, typename ItemViewT>
RimNestedMirrorCollectionInView<SelfT, SourceT, ItemViewT>::RimNestedMirrorCollectionInView()
{
// m_itemsInView, m_collectionsInView and m_sourceCollection must be initialized by the
// derived class using CAF_PDM_InitFieldNoDefault, so that the XML keywords are stable
// for that class.
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
template <typename SelfT, typename SourceT, typename ItemViewT>
RimNestedMirrorCollectionInView<SelfT, SourceT, ItemViewT>::~RimNestedMirrorCollectionInView()
{
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
template <typename SelfT, typename SourceT, typename ItemViewT>
SourceT* RimNestedMirrorCollectionInView<SelfT, SourceT, ItemViewT>::sourceCollection() const
{
return m_sourceCollection();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
template <typename SelfT, typename SourceT, typename ItemViewT>
void RimNestedMirrorCollectionInView<SelfT, SourceT, ItemViewT>::setSourceCollection( SourceT* source )
{
m_sourceCollection = source;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
template <typename SelfT, typename SourceT, typename ItemViewT>
void RimNestedMirrorCollectionInView<SelfT, SourceT, ItemViewT>::updateFromSource()
{
updateAllViewItems();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
template <typename SelfT, typename SourceT, typename ItemViewT>
SelfT* RimNestedMirrorCollectionInView<SelfT, SourceT, ItemViewT>::createSubCollectionInView( SourceT* src )
{
auto* sub = new SelfT();
sub->setSourceCollection( src );
return sub;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
template <typename SelfT, typename SourceT, typename ItemViewT>
SelfT* RimNestedMirrorCollectionInView<SelfT, SourceT, ItemViewT>::findCollectionInViewForSource( const SourceT* src ) const
{
for ( auto coll : m_collectionsInView )
{
if ( coll && coll->sourceCollection() == src ) return coll;
}
return nullptr;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
template <typename SelfT, typename SourceT, typename ItemViewT>
ItemViewT* RimNestedMirrorCollectionInView<SelfT, SourceT, ItemViewT>::findItemInViewForSource( const SourceItemT* src ) const
{
for ( auto item : m_itemsInView )
{
if ( item && item->sourceItem() == src ) return item;
}
return nullptr;
}
//--------------------------------------------------------------------------------------------------
/// Recursive sync of sub-collections and items, mirroring the source tree.
///
/// Step 1 - sub-collections: drop wrappers whose source pointer is null, then iterate
/// sourceSubCollections() and find-or-create one wrapper per source, reordering to match.
/// Recurse into each child so nested levels mirror as well.
///
/// Step 2 - items: same shape. Drop orphans, iterate sourceItems(), find-or-create.
//--------------------------------------------------------------------------------------------------
template <typename SelfT, typename SourceT, typename ItemViewT>
void RimNestedMirrorCollectionInView<SelfT, SourceT, ItemViewT>::updateAllViewItems()
{
// Refresh display name from source so the view tree label tracks the source.
if ( m_sourceCollection )
{
this->setName( m_sourceCollection->collectionName() );
}
// --- Sub-collections ---
{
std::vector<SelfT*> existing = m_collectionsInView.childrenByType();
for ( auto* coll : existing )
{
if ( !coll->sourceCollection() )
{
m_collectionsInView.removeChild( coll );
delete coll;
}
}
std::vector<SelfT*> ordered;
if ( m_sourceCollection )
{
for ( auto* srcSub : sourceSubCollections() )
{
if ( !srcSub ) continue;
SelfT* viewSub = findCollectionInViewForSource( srcSub );
if ( viewSub == nullptr )
{
viewSub = createSubCollectionInView( srcSub );
}
ordered.push_back( viewSub );
}
}
m_collectionsInView.clearWithoutDelete();
for ( auto* viewSub : ordered )
{
m_collectionsInView.push_back( viewSub );
viewSub->updateAllViewItems();
}
}
// --- Items ---
{
std::vector<ItemViewT*> existing = m_itemsInView.childrenByType();
for ( auto* item : existing )
{
if ( !item->sourceItem() )
{
m_itemsInView.removeChild( item );
delete item;
}
}
std::vector<ItemViewT*> ordered;
if ( m_sourceCollection )
{
for ( auto* srcItem : sourceItems() )
{
if ( !srcItem ) continue;
ItemViewT* viewItem = findItemInViewForSource( srcItem );
if ( viewItem == nullptr )
{
viewItem = createItemInView( srcItem );
}
ordered.push_back( viewItem );
}
}
m_itemsInView.clearWithoutDelete();
for ( auto* viewItem : ordered )
{
m_itemsInView.push_back( viewItem );
}
}
this->updateConnectedEditors();
}
@@ -25,6 +25,10 @@ set(PROJECT_FILES
cafPdmObject.h
cafPdmObjectCollection.h
cafPdmObjectCollection.inl
cafPdmNestedCollection.h
cafPdmNestedCollection.inl
cafPdmNestedCollectionBase.h
cafPdmNestedCollectionBase.cpp
)
add_library(${PROJECT_NAME} ${PROJECT_FILES})
@@ -0,0 +1,101 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#pragma once
#include "cafPdmChildArrayField.h"
#include "cafPdmField.h"
#include "cafPdmNestedCollectionBase.h"
#include "cafPdmObjectCollection.h" // for DerivedFromPdmObject concept
#include <QString>
#include <vector>
namespace caf
{
//==================================================================================================
///
/// Generic templated nested collection base class
///
/// CRTP template for tree-shaped PDM containers (folder of items + folder of folders). Derives
/// from caf::PdmNestedCollectionBase, which provides the non-templated PdmObject anchor (so
/// generic script methods like AddFolder can be registered once on the base and inherit through
/// every concrete derivation).
///
/// Template parameters:
/// SelfT - The derived class (CRTP). Must inherit from caf::PdmObject.
/// ItemT - Item type held in the flat items vector. Must inherit from caf::PdmObject.
///
/// Derived classes must call CAF_PDM_InitFieldNoDefault for the inherited fields
/// m_items, m_collectionName and m_subCollections so the XML keywords stay derived-class
/// specific (matching the existing PdmObjectCollection<T> convention).
///
//==================================================================================================
template <typename SelfT, typename ItemT>
class PdmNestedCollection : public PdmNestedCollectionBase
{
public:
// Item access
std::vector<ItemT*> items() const;
size_t count() const;
bool isEmpty() const;
PdmChildArrayField<ItemT*>& itemsField();
const PdmChildArrayField<ItemT*>& itemsField() const;
// Item CRUD
void addItem( ItemT* item );
void insertItem( ItemT* insertBefore, ItemT* item );
void deleteItem( ItemT* item );
void deleteAllItems();
ItemT* createDefaultItem();
void updateConnectedEditors();
// Subcollection access
std::vector<SelfT*> subCollections() const;
// Subcollection CRUD
void addSubCollection( SelfT* sub );
PdmObject* addNewSubCollection() override;
SelfT* findSubCollectionByName( const QString& name ) const;
// Returns items held by this collection and recursively by all subcollections.
// For items at this level only, use items().
std::vector<ItemT*> allItems() const;
protected:
PdmNestedCollection();
~PdmNestedCollection() override;
// Hook for derived classes; called when m_items changes.
virtual void onItemsChanged();
void fieldChangedByUi( const PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue ) override;
void defineUiOrdering( QString uiConfigName, PdmUiOrdering& uiOrdering ) override;
void defineEditorAttribute( const PdmFieldHandle* field, QString uiConfigName, PdmUiEditorAttribute* attribute ) override;
void onChildDeleted( PdmChildArrayFieldHandle* childArray, std::vector<PdmObjectHandle*>& referringObjects ) override;
PdmChildArrayField<SelfT*> m_subCollections;
PdmChildArrayField<ItemT*> m_items;
};
} // namespace caf
#include "cafPdmNestedCollection.inl"
@@ -0,0 +1,296 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "cafPdmUiTableViewEditor.h"
namespace caf
{
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
template <typename SelfT, typename ItemT>
PdmNestedCollection<SelfT, ItemT>::PdmNestedCollection()
{
static_assert( DerivedFromPdmObject<SelfT>, "SelfT must inherit from caf::PdmObject" );
static_assert( DerivedFromPdmObject<ItemT>, "ItemT must inherit from caf::PdmObject" );
// m_items, m_collectionName and m_subCollections must be initialized by the derived class
// using CAF_PDM_InitFieldNoDefault, so that the XML keywords are stable for that class.
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
template <typename SelfT, typename ItemT>
PdmNestedCollection<SelfT, ItemT>::~PdmNestedCollection()
{
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
template <typename SelfT, typename ItemT>
std::vector<ItemT*> PdmNestedCollection<SelfT, ItemT>::items() const
{
return m_items.childrenByType();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
template <typename SelfT, typename ItemT>
size_t PdmNestedCollection<SelfT, ItemT>::count() const
{
return m_items.size();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
template <typename SelfT, typename ItemT>
bool PdmNestedCollection<SelfT, ItemT>::isEmpty() const
{
return m_items.empty();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
template <typename SelfT, typename ItemT>
PdmChildArrayField<ItemT*>& PdmNestedCollection<SelfT, ItemT>::itemsField()
{
return m_items;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
template <typename SelfT, typename ItemT>
const PdmChildArrayField<ItemT*>& PdmNestedCollection<SelfT, ItemT>::itemsField() const
{
return m_items;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
template <typename SelfT, typename ItemT>
void PdmNestedCollection<SelfT, ItemT>::addItem( ItemT* item )
{
if ( item )
{
m_items.push_back( item );
onItemsChanged();
updateConnectedEditors();
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
template <typename SelfT, typename ItemT>
void PdmNestedCollection<SelfT, ItemT>::insertItem( ItemT* insertBefore, ItemT* item )
{
if ( !item ) return;
size_t index = m_items.indexOf( insertBefore );
if ( index < m_items.size() )
m_items.insert( index, item );
else
m_items.push_back( item );
onItemsChanged();
updateConnectedEditors();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
template <typename SelfT, typename ItemT>
void PdmNestedCollection<SelfT, ItemT>::deleteItem( ItemT* item )
{
if ( item )
{
m_items.removeChild( item );
delete item;
updateConnectedEditors();
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
template <typename SelfT, typename ItemT>
void PdmNestedCollection<SelfT, ItemT>::deleteAllItems()
{
m_items.deleteChildren();
updateConnectedEditors();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
template <typename SelfT, typename ItemT>
ItemT* PdmNestedCollection<SelfT, ItemT>::createDefaultItem()
{
auto* item = new ItemT();
addItem( item );
return item;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
template <typename SelfT, typename ItemT>
void PdmNestedCollection<SelfT, ItemT>::updateConnectedEditors()
{
this->uiCapability()->updateConnectedEditors();
m_items.uiCapability()->updateConnectedEditors();
m_subCollections.uiCapability()->updateConnectedEditors();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
template <typename SelfT, typename ItemT>
std::vector<SelfT*> PdmNestedCollection<SelfT, ItemT>::subCollections() const
{
return m_subCollections.childrenByType();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
template <typename SelfT, typename ItemT>
void PdmNestedCollection<SelfT, ItemT>::addSubCollection( SelfT* sub )
{
if ( sub )
{
m_subCollections.push_back( sub );
this->updateConnectedEditors();
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
template <typename SelfT, typename ItemT>
PdmObject* PdmNestedCollection<SelfT, ItemT>::addNewSubCollection()
{
auto* sub = new SelfT();
addSubCollection( sub );
return sub;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
template <typename SelfT, typename ItemT>
SelfT* PdmNestedCollection<SelfT, ItemT>::findSubCollectionByName( const QString& name ) const
{
for ( auto coll : m_subCollections )
{
if ( coll && coll->collectionName() == name ) return coll;
}
return nullptr;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
template <typename SelfT, typename ItemT>
std::vector<ItemT*> PdmNestedCollection<SelfT, ItemT>::allItems() const
{
std::vector<ItemT*> result = items();
for ( auto* sub : subCollections() )
{
if ( !sub ) continue;
auto subItems = sub->allItems();
result.insert( result.end(), subItems.begin(), subItems.end() );
}
return result;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
template <typename SelfT, typename ItemT>
void PdmNestedCollection<SelfT, ItemT>::onItemsChanged()
{
// Default implementation does nothing; derived classes may override.
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
template <typename SelfT, typename ItemT>
void PdmNestedCollection<SelfT, ItemT>::fieldChangedByUi( const PdmFieldHandle* changedField,
const QVariant& oldValue,
const QVariant& newValue )
{
if ( changedField == &m_items )
{
onItemsChanged();
}
updateConnectedEditors();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
template <typename SelfT, typename ItemT>
void PdmNestedCollection<SelfT, ItemT>::defineUiOrdering( QString uiConfigName, PdmUiOrdering& uiOrdering )
{
uiOrdering.add( &m_collectionName );
uiOrdering.add( &m_subCollections );
uiOrdering.add( &m_items );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
template <typename SelfT, typename ItemT>
void PdmNestedCollection<SelfT, ItemT>::defineEditorAttribute( const PdmFieldHandle* field,
QString uiConfigName,
PdmUiEditorAttribute* attribute )
{
if ( field == &m_items )
{
auto tvAttribute = dynamic_cast<PdmUiTableViewEditorAttribute*>( attribute );
if ( tvAttribute )
{
tvAttribute->resizePolicy = PdmUiTableViewEditorAttribute::RESIZE_TO_FILL_CONTAINER;
tvAttribute->alwaysEnforceResizePolicy = true;
tvAttribute->minimumHeight = 300;
}
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
template <typename SelfT, typename ItemT>
void PdmNestedCollection<SelfT, ItemT>::onChildDeleted( PdmChildArrayFieldHandle* childArray,
std::vector<PdmObjectHandle*>& referringObjects )
{
updateConnectedEditors();
}
} // namespace caf
@@ -0,0 +1,100 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "cafPdmNestedCollectionBase.h"
#include "cafIconProvider.h"
#include <QApplication>
#include <QStyle>
namespace caf
{
CAF_PDM_ABSTRACT_SOURCE_INIT( PdmNestedCollectionBase, "PdmNestedCollectionBase" );
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
PdmNestedCollectionBase::PdmNestedCollectionBase()
: m_isTopmostFolder( false )
{
CAF_PDM_InitObject( "Nested Collection" );
// m_collectionName is initialized by derived classes with a derived-specific XML keyword,
// matching the existing per-class scriptable-field convention.
// Apply the platform-native folder icon as the per-instance default so every nested
// collection looks like a folder in the project tree. Derived classes that want a branded
// icon (e.g. the top-level instance) override via setUiIcon / createTopmost(). In console
// mode there is no QApplication / QStyle, so we fall back to whatever static icon the
// derived class supplied to CAF_PDM_InitObject.
if ( qobject_cast<QApplication*>( QApplication::instance() ) )
{
if ( QStyle* style = QApplication::style() )
{
QPixmap pix = style->standardIcon( QStyle::SP_DirIcon ).pixmap( 16, 16 );
uiCapability()->setUiIcon( IconProvider( pix ) );
}
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
PdmNestedCollectionBase::~PdmNestedCollectionBase()
{
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
QString PdmNestedCollectionBase::collectionName() const
{
return m_collectionName.value();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void PdmNestedCollectionBase::setCollectionName( const QString& name )
{
m_collectionName.setValue( name );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void PdmNestedCollectionBase::setAsTopmostFolder()
{
m_collectionName.uiCapability()->setUiHidden( true );
m_collectionName.xmlCapability()->disableIO();
setDeletable( false );
m_isTopmostFolder = true;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
PdmFieldHandle* PdmNestedCollectionBase::userDescriptionField()
{
if ( m_isTopmostFolder ) return nullptr;
return &m_collectionName;
}
} // namespace caf
@@ -0,0 +1,74 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#pragma once
#include "cafPdmField.h"
#include "cafPdmObject.h"
#include <QString>
namespace caf
{
//==================================================================================================
///
/// Non-templated PdmObject base class for tree-shaped containers.
///
/// Provides the type-erased interface (collection name, add-new-subcollection, leaf check) that
/// generic command features and script methods need. The templated caf::PdmNestedCollection<>
/// derives from this and supplies typed storage and accessors.
///
/// Concrete (instantiable) so the CAF factory can create a dummy instance for the Python
/// generator. The base's addNewSubCollection() is a no-op returning nullptr; the templated
/// derived class overrides it with a real implementation.
///
//==================================================================================================
class PdmNestedCollectionBase : public PdmObject
{
CAF_PDM_HEADER_INIT;
public:
PdmNestedCollectionBase();
~PdmNestedCollectionBase() override;
QString collectionName() const;
void setCollectionName( const QString& name );
// Whether this container accepts sub-collections. Leaf containers (e.g. file-backed
// folders) override to return false; generic UI / script features hide the corresponding
// action when this is false.
virtual bool canAddSubCollection() const { return true; }
// Creates a subcollection, adds it to this collection, and returns it. Default returns
// nullptr; concrete derivations (typically PdmNestedCollection<>) override.
virtual PdmObject* addNewSubCollection() { return nullptr; }
// Marks this instance as the topmost folder: hides and disables IO on the collection name,
// makes the object non-deletable.
void setAsTopmostFolder();
protected:
PdmFieldHandle* userDescriptionField() override;
PdmField<QString> m_collectionName;
bool m_isTopmostFolder;
};
} // namespace caf
+81
View File
@@ -0,0 +1,81 @@
# `caf::PdmNestedCollection` — design notes
## Context
`caf::PdmNestedCollection` is the base class for tree-shaped PDM containers — a
folder of items that can also contain folders of the same kind. The design is a
non-template concrete base + a CRTP template:
```cpp
class PdmNestedCollectionBase : public PdmObject
{
QString collectionName() const;
void setCollectionName( const QString& name );
virtual bool canAddSubCollection() const;
virtual PdmObject* addNewSubCollection();
void setAsTopmostFolder();
PdmField<QString> m_collectionName;
};
template <typename SelfT, typename ItemT>
class PdmNestedCollection : public PdmNestedCollectionBase
{
// typed accessors + storage for items and subcollections
PdmChildArrayField<SelfT*> m_subCollections;
PdmChildArrayField<ItemT*> m_items;
};
```
`PdmNestedCollectionBase` is the shared PdmObject ancestor — the anchor point
where the CAF method factory can register generic script methods (like
`AddFolder`) once and have them surface on every derived nested collection via
the inheritance walk and Python class hierarchy.
`PdmNestedCollection<SelfT, ItemT>` adds typed storage and accessors. `SelfT` is
the CRTP self type (so subcollections are typed `SelfT*`); `ItemT` is the leaf
item type. The template does not inherit `PdmObjectCollection<ItemT>` — m_items
is declared here directly, so the inheritance chain stays linear and free of
diamond shapes.
## Why both a non-template base and a CRTP template
Two needs that don't fit into a single layer:
- **CAF method registration is keyed by `classKeyword`** — only a concrete
non-template class can serve as the anchor. Hence the non-template
`PdmNestedCollectionBase`. Without it, generic script methods (`AddFolder`)
would have to be re-registered per derived class.
- **Typed storage and accessors** — `m_subCollections : PdmChildArrayField<SelfT*>`
needs `SelfT`, which only exists as a template parameter. Hence the CRTP
template layer on top of the base.
## Alternative considered: non-templated base only
```cpp
class PdmNestedCollection : public PdmObject
{
...
virtual PdmObject* createSubCollection() const = 0;
// m_subCollectionsField / m_itemsField stored as type-erased handles
};
```
Each derived class would own its typed `PdmChildArrayField<DerivedType*>
m_subCollections` and `PdmChildArrayField<ItemType*> m_items`, register them
with the base in the constructor, and override `createSubCollection()`. About
~25 lines of boilerplate per derived class beyond what the current design needs:
two field declarations, two `registerXxxField` calls, an override of
`createSubCollection`, re-declared typed accessors.
At the projected 8–10 derived classes that's ~200–250 lines of repeated code.
The hybrid (non-template base + template) design pays the cost once in the
framework and gets typed access plus generic registration for free.
## When to revisit
- If the count of nested-collection types drops to 1–2 — the template tier no
longer earns its keep, and a pure non-template base wins on simplicity.
- If template instantiation cost shows up in build profiling for the PDM layer.
- If the typed accessors are rarely used in practice and most call sites
immediately cast to `PdmObject*` anyway.