#14411 Enforce name uniqueness for surfaces, polygons and folders

* #14411 Enforce name uniqueness for surfaces, polygons and folders

Names must be unique among siblings sharing the same parent folder. Items in different folders may keep identical names, and comparison is case sensitive.

Siblings are the objects held by the same caf::PdmChildArrayField. Items and folders live in two distinct child arrays in caf::PdmNestedCollection, so a folder and an item may share a name under the same parent.

The Python API gets an OnNameConflict flag on AddFolder, CreatePolygon, ImportSurface and NewRegularSurface, supporting FAIL (default), AUTO_RENAME and OVERWRITE. NewSurface is left out, as grid case surfaces derive their tree label from the case and K index and carry no name of their own.
This commit is contained in:
Magne Sjaastad authored and GitHub committed 2026-08-07 08:34:58 +02:00
1 parent 5e791353b2
commit ebf75c09a1
28 files changed
+889 -26

No files matched your search

@@ -179,6 +179,15 @@ void caf::AppEnum<RiaDefines::RowCount>::setUp()
setDefault( RiaDefines::RowCount::ROWS_2 );
}
template <>
void caf::AppEnum<RiaDefines::NameConflictPolicy>::setUp()
{
addItem( RiaDefines::NameConflictPolicy::FAIL, "FAIL", "Fail" );
addItem( RiaDefines::NameConflictPolicy::AUTO_RENAME, "AUTO_RENAME", "Auto Rename" );
addItem( RiaDefines::NameConflictPolicy::OVERWRITE, "OVERWRITE", "Overwrite" );
setDefault( RiaDefines::NameConflictPolicy::FAIL );
}
} // namespace caf
namespace
@@ -192,6 +201,7 @@ struct RegisterScriptEnumNames
{
caf::PdmScriptEnumNameRegistry::registerName<RiaDefines::ResultCatType>( "PropertyType" );
caf::PdmScriptEnumNameRegistry::registerName<RiaDefines::PorosityModelType>( "PorosityModelType" );
caf::PdmScriptEnumNameRegistry::registerName<RiaDefines::NameConflictPolicy>( "NameConflictPolicy" );
}
};
const RegisterScriptEnumNames s_registerScriptEnumNames;
@@ -211,6 +211,15 @@ enum class WellProductionType : short
bool isInjector( WellProductionType wellProductionType );
// How to handle a new item colliding with the name of an existing item in the same folder.
// Used by the Python/gRPC API, where no user is present to resolve the conflict interactively.
enum class NameConflictPolicy
{
FAIL, // Abort the operation and report an error
AUTO_RENAME, // Append a numeric suffix to make the name unique
OVERWRITE // Delete the existing item carrying the name
};
QString stringListSeparator();
enum class ColumnCount
@@ -36,6 +36,7 @@ set(SOURCE_GROUP_HEADER_FILES
${CMAKE_CURRENT_LIST_DIR}/RiaWeightedMeanCalculator.inl
${CMAKE_CURRENT_LIST_DIR}/RiaWeightedGeometricMeanCalculator.h
${CMAKE_CURRENT_LIST_DIR}/RiaWeightedHarmonicMeanCalculator.h
${CMAKE_CURRENT_LIST_DIR}/RiaNameUniquenessTools.h
${CMAKE_CURRENT_LIST_DIR}/RiaOptionItemFactory.h
${CMAKE_CURRENT_LIST_DIR}/RiaGitDiff.h
${CMAKE_CURRENT_LIST_DIR}/RiaWslTools.h
@@ -94,6 +95,7 @@ set(SOURCE_GROUP_SOURCE_FILES
${CMAKE_CURRENT_LIST_DIR}/RiaTimeHistoryCurveResampler.cpp
${CMAKE_CURRENT_LIST_DIR}/RiaWeightedGeometricMeanCalculator.cpp
${CMAKE_CURRENT_LIST_DIR}/RiaWeightedHarmonicMeanCalculator.cpp
${CMAKE_CURRENT_LIST_DIR}/RiaNameUniquenessTools.cpp
${CMAKE_CURRENT_LIST_DIR}/RiaOptionItemFactory.cpp
${CMAKE_CURRENT_LIST_DIR}/RiaGitDiff.cpp
${CMAKE_CURRENT_LIST_DIR}/RiaWslTools.cpp
@@ -0,0 +1,262 @@
/////////////////////////////////////////////////////////////////////////////////
//
// 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 "RiaNameUniquenessTools.h"
#include "RiaLogging.h"
#include "RimNamedObject.h"
#include "RimSurface.h"
#include "cafPdmChildArrayField.h"
#include "cafPdmNestedCollectionBase.h"
#include "cafPdmObjectHandle.h"
#include "cafPdmUiObjectHandle.h"
#include "cafPdmValueField.h"
#include <QVariant>
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
QString RiaNameUniquenessTools::objectName( const caf::PdmObjectHandle* object )
{
if ( !object ) return {};
auto* mutableObject = const_cast<caf::PdmObjectHandle*>( object );
if ( auto* surface = dynamic_cast<RimSurface*>( mutableObject ) ) return surface->userDescription();
if ( auto* namedObject = dynamic_cast<RimNamedObject*>( mutableObject ) ) return namedObject->name();
if ( auto* collection = dynamic_cast<caf::PdmNestedCollectionBase*>( mutableObject ) ) return collection->collectionName();
if ( auto* uiObjectHandle = caf::uiObj( object ) )
{
if ( auto* valueField = dynamic_cast<caf::PdmValueField*>( uiObjectHandle->userDescriptionField() ) )
{
return valueField->toQVariant().toString();
}
}
return {};
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RiaNameUniquenessTools::setObjectName( caf::PdmObjectHandle* object, const QString& name )
{
if ( !object ) return;
if ( auto* surface = dynamic_cast<RimSurface*>( object ) )
{
surface->setUserDescription( name );
return;
}
if ( auto* namedObject = dynamic_cast<RimNamedObject*>( object ) )
{
namedObject->setName( name );
return;
}
if ( auto* collection = dynamic_cast<caf::PdmNestedCollectionBase*>( object ) )
{
collection->setCollectionName( name );
return;
}
if ( auto* uiObjectHandle = caf::uiObj( object ) )
{
if ( auto* valueField = dynamic_cast<caf::PdmValueField*>( uiObjectHandle->userDescriptionField() ) )
{
valueField->setFromQVariant( name );
}
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::set<QString> RiaNameUniquenessTools::namesInCollection( const caf::PdmChildArrayFieldHandle* childArrayField,
const caf::PdmObjectHandle* objectToExclude )
{
std::set<QString> names;
if ( !childArrayField ) return names;
auto* mutableField = const_cast<caf::PdmChildArrayFieldHandle*>( childArrayField );
for ( size_t i = 0; i < mutableField->size(); i++ )
{
auto* child = mutableField->at( i );
if ( !child || child == objectToExclude ) continue;
names.insert( objectName( child ) );
}
return names;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::set<QString> RiaNameUniquenessTools::siblingNames( const caf::PdmObjectHandle* object )
{
if ( !object ) return {};
auto* childArrayField = dynamic_cast<caf::PdmChildArrayFieldHandle*>( object->parentField() );
if ( !childArrayField ) return {};
return namesInCollection( childArrayField, object );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
QString RiaNameUniquenessTools::makeUnique( const QString& candidateName, const std::set<QString>& takenNames )
{
// An empty name means the object has no name of its own, and derives its tree label from
// other properties (a grid case surface is labelled from its case and K index). Numbering
// those would only produce labels like "_1 - K:3".
if ( candidateName.isEmpty() ) return candidateName;
if ( !takenNames.contains( candidateName ) ) return candidateName;
int counter = 1;
QString uniqueName;
do
{
uniqueName = QString( "%1_%2" ).arg( candidateName ).arg( counter++ );
} while ( takenNames.contains( uniqueName ) );
return uniqueName;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
bool RiaNameUniquenessTools::isUniqueAmongSiblings( const caf::PdmObjectHandle* object, const QString& candidateName )
{
if ( candidateName.isEmpty() ) return true;
return !siblingNames( object ).contains( candidateName );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
QString RiaNameUniquenessTools::makeUniqueAmongSiblings( const caf::PdmObjectHandle* object, const QString& candidateName )
{
return makeUnique( candidateName, siblingNames( object ) );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
QString RiaNameUniquenessTools::ensureUniqueAmongSiblings( caf::PdmObjectHandle* object )
{
const QString currentName = objectName( object );
const QString uniqueName = makeUniqueAmongSiblings( object, currentName );
if ( uniqueName != currentName ) setObjectName( object, uniqueName );
return uniqueName;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
QString RiaNameUniquenessTools::resolveRenameConflict( const caf::PdmObjectHandle* object, const QString& desiredName )
{
const QString uniqueName = makeUniqueAmongSiblings( object, desiredName );
if ( uniqueName != desiredName )
{
RiaLogging::info(
QString( "\"%1\" already exists in this folder, using \"%2\" instead." ).arg( desiredName ).arg( uniqueName ).toStdString() );
}
return uniqueName;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
bool RiaNameUniquenessTools::isUniqueInCollection( const caf::PdmChildArrayFieldHandle* childArrayField, const QString& candidateName )
{
if ( candidateName.isEmpty() ) return true;
return !namesInCollection( childArrayField ).contains( candidateName );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
QString RiaNameUniquenessTools::makeUniqueInCollection( const caf::PdmChildArrayFieldHandle* childArrayField, const QString& candidateName )
{
return makeUnique( candidateName, namesInCollection( childArrayField ) );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
caf::PdmObjectHandle* RiaNameUniquenessTools::findInCollection( const caf::PdmChildArrayFieldHandle* childArrayField, const QString& name )
{
if ( !childArrayField ) return nullptr;
auto* mutableField = const_cast<caf::PdmChildArrayFieldHandle*>( childArrayField );
for ( size_t i = 0; i < mutableField->size(); i++ )
{
auto* child = mutableField->at( i );
if ( child && objectName( child ) == name ) return child;
}
return nullptr;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RiaNameUniquenessTools::ConflictResolution RiaNameUniquenessTools::applyConflictPolicy( const caf::PdmChildArrayFieldHandle* childArrayField,
const QString& candidateName,
RiaDefines::NameConflictPolicy policy )
{
ConflictResolution resolution;
resolution.nameToUse = candidateName;
if ( candidateName.isEmpty() ) return resolution;
auto* existingObject = findInCollection( childArrayField, candidateName );
if ( !existingObject ) return resolution;
switch ( policy )
{
case RiaDefines::NameConflictPolicy::AUTO_RENAME:
resolution.nameToUse = makeUniqueInCollection( childArrayField, candidateName );
break;
case RiaDefines::NameConflictPolicy::OVERWRITE:
resolution.objectToReplace = existingObject;
break;
case RiaDefines::NameConflictPolicy::FAIL:
default:
resolution.errorMessage = QString( "An item named '%1' already exists in this folder" ).arg( candidateName );
break;
}
return resolution;
}
@@ -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 "RiaDefines.h"
#include <QString>
#include <set>
namespace caf
{
class PdmChildArrayFieldHandle;
class PdmObjectHandle;
} // namespace caf
//==================================================================================================
///
/// Helpers used to keep names unique among siblings in the project tree.
///
/// Siblings are the objects held by the same caf::PdmChildArrayField. Items and folders live in
/// two distinct child arrays (m_items and m_subCollections in caf::PdmNestedCollection), so a
/// folder and an item may carry the same name under the same parent. Names in different folders
/// never conflict.
///
/// All comparison is case sensitive.
///
//==================================================================================================
namespace RiaNameUniquenessTools
{
// Name of an object as shown in the project tree. Knows about the name field of surfaces,
// named objects and nested collections, and falls back to the user description field.
QString objectName( const caf::PdmObjectHandle* object );
void setObjectName( caf::PdmObjectHandle* object, const QString& name );
// Names of the objects in a child array field, optionally excluding one object.
std::set<QString> namesInCollection( const caf::PdmChildArrayFieldHandle* childArrayField,
const caf::PdmObjectHandle* objectToExclude = nullptr );
// Names used by the other objects in the child array field holding this object.
std::set<QString> siblingNames( const caf::PdmObjectHandle* object );
// Appends _1, _2, ... to candidateName until the result is not present in takenNames.
QString makeUnique( const QString& candidateName, const std::set<QString>& takenNames );
bool isUniqueAmongSiblings( const caf::PdmObjectHandle* object, const QString& candidateName );
QString makeUniqueAmongSiblings( const caf::PdmObjectHandle* object, const QString& candidateName );
// Renames the object if one of its siblings already carries the same name. The object must
// already be inserted into its parent collection. Returns the name in effect after the call.
QString ensureUniqueAmongSiblings( caf::PdmObjectHandle* object );
// Returns the name to apply when the user renames an object. If desiredName is already unique
// among the siblings of the object, it is returned unchanged. Otherwise an auto-generated unique
// name is returned, and the substitution is written to the log.
QString resolveRenameConflict( const caf::PdmObjectHandle* object, const QString& desiredName );
// For objects not yet inserted into a parent collection.
bool isUniqueInCollection( const caf::PdmChildArrayFieldHandle* childArrayField, const QString& candidateName );
QString makeUniqueInCollection( const caf::PdmChildArrayFieldHandle* childArrayField, const QString& candidateName );
// The object in the child array field carrying this exact name, or nullptr.
caf::PdmObjectHandle* findInCollection( const caf::PdmChildArrayFieldHandle* childArrayField, const QString& name );
//==================================================================================================
/// Outcome of applying a name conflict policy. Exactly one of the three states applies:
/// - errorMessage set : the policy is FAIL and the name is taken, abort the operation
/// - objectToReplace set : the policy is OVERWRITE, the caller must delete this object
/// - neither set : use nameToUse, which is unique
//==================================================================================================
struct ConflictResolution
{
QString nameToUse;
caf::PdmObjectHandle* objectToReplace = nullptr;
QString errorMessage;
};
// Resolves a name conflict for a name about to be used in childArrayField. Deleting the object
// to replace is left to the caller, as the removal must go through the owning collection to keep
// views and referring objects in sync.
ConflictResolution applyConflictPolicy( const caf::PdmChildArrayFieldHandle* childArrayField,
const QString& candidateName,
RiaDefines::NameConflictPolicy policy );
}; // namespace RiaNameUniquenessTools