#14192 Python: Update discrete property color legend in place instead of delete/create

Address review feedback on #14199: deleting and recreating the legend left
views bound to the old legend pointing at a deleted object. Add
RimColorLegendCollection::updateColorLegend and a matching UpdateColorLegend
scriptable method that mutate the existing custom legend (name and items) so
referring views keep their binding and are notified once. A new legend is only
created when none is registered for the result or the registered legend is a
standard legend.

set_discrete_property_category_names now performs a single atomic call instead
of delete + create + per-item appends. The empty-dict removal path still uses
DeleteColorLegend.
This commit is contained in:
Kristian Bendiksen
2026-06-10 18:02:07 +02:00
committed by Magne Sjaastad
parent 5a083331b0
commit a4fe22508a
8 changed files with 200 additions and 17 deletions
@@ -101,6 +101,21 @@ void RimColorLegend::appendColorLegendItem( RimColorLegendItem* colorLegendItem
onColorLegendItemHasChanged();
}
//--------------------------------------------------------------------------------------------------
/// Replace all color legend items. Takes ownership of the new items.
//--------------------------------------------------------------------------------------------------
void RimColorLegend::setColorLegendItems( const std::vector<RimColorLegendItem*>& colorLegendItems )
{
m_colorLegendItems.deleteChildren();
for ( auto colorLegendItem : colorLegendItems )
{
m_colorLegendItems.push_back( colorLegendItem );
}
onColorLegendItemHasChanged();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
@@ -54,6 +54,7 @@ public:
void addReorderCapability();
void appendColorLegendItem( RimColorLegendItem* colorLegendItem );
void setColorLegendItems( const std::vector<RimColorLegendItem*>& colorLegendItems );
std::vector<RimColorLegendItem*> colorLegendItems() const;
cvf::Color3ubArray colorArray() const;
@@ -114,6 +114,50 @@ RimColorLegend* RimColorLegendCollection::createColorLegend( const QString& colo
return colorLegend;
}
//--------------------------------------------------------------------------------------------------
/// Update the custom color legend registered as default for the given result in place, so that
/// objects referring to the legend keep their binding. Creates and registers a new custom legend
/// if none exists or the registered legend is a standard legend. If colors is empty, palette
/// colors are assigned automatically.
//--------------------------------------------------------------------------------------------------
RimColorLegend* RimColorLegendCollection::updateColorLegend( const RimCase* rimCase,
const QString& resultName,
const QString& colorLegendName,
const std::vector<int>& categoryValues,
const std::vector<QString>& categoryNames,
const std::vector<cvf::Color3f>& colors )
{
CAF_ASSERT( categoryValues.size() == categoryNames.size() );
CAF_ASSERT( colors.empty() || colors.size() == categoryValues.size() );
auto legend = findDefaultLegendForResult( rimCase, resultName );
if ( !legend || isStandardColorLegend( legend ) )
{
legend = new RimColorLegend();
appendCustomColorLegend( legend );
}
legend->setColorLegendName( colorLegendName );
auto paletteColors = RiaColorTables::categoryPaletteColors().color3ubArray();
std::vector<RimColorLegendItem*> items;
for ( size_t i = 0; i < categoryValues.size(); i++ )
{
cvf::Color3f color = colors.empty() ? cvf::Color3f( paletteColors[i % paletteColors.size()] ) : colors[i];
auto item = new RimColorLegendItem();
item->setValues( categoryNames[i], categoryValues[i], color );
items.push_back( item );
}
legend->setColorLegendItems( items );
setDefaultColorLegendForResult( rimCase, resultName, legend );
return legend;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
@@ -22,6 +22,8 @@
#include "cafPdmField.h"
#include "cafPdmObject.h"
#include "cvfColor3.h"
class RimCase;
class RimColorLegend;
class RimColorLegendItem;
@@ -50,6 +52,12 @@ public:
void deleteCustomColorLegends();
RimColorLegend* createColorLegend( const QString& colorLegendName, const std::map<int, QString>& valuesAndNames );
RimColorLegend* updateColorLegend( const RimCase* rimCase,
const QString& resultName,
const QString& colorLegendName,
const std::vector<int>& categoryValues,
const std::vector<QString>& categoryNames,
const std::vector<cvf::Color3f>& colors );
void createColorLegendFromFormationNames( RimFormationNames* rimFormationNames );
void deleteColorLegend( const RimCase* rimCase, const QString& resultName );
void setDefaultColorLegendForResult( const RimCase* rimCase, const QString& resultName, RimColorLegend* colorLegend );
@@ -105,6 +105,54 @@ QString RimcColorLegend_addColorLegendItem::classKeywordReturnedType() const
return RimColorLegendItem::classKeywordStatic();
}
CAF_PDM_OBJECT_METHOD_SOURCE_INIT( RimColorLegendCollection, RimcColorLegendCollection_updateColorLegend, "UpdateColorLegend" );
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RimcColorLegendCollection_updateColorLegend::RimcColorLegendCollection_updateColorLegend( caf::PdmObjectHandle* self )
: caf::PdmObjectCreationMethod( self )
{
CAF_PDM_InitObject( "Update Color Legend", "", "", "Update the color legend bound to a (case, resultName) pair in place, creating it if needed" );
CAF_PDM_InitScriptableFieldNoDefault( &m_case, "Case", "Case" );
CAF_PDM_InitScriptableField( &m_resultName, "ResultName", QString(), "Result Name" );
CAF_PDM_InitScriptableField( &m_legendName, "LegendName", QString(), "Legend Name" );
CAF_PDM_InitScriptableFieldNoDefault( &m_categoryValues, "CategoryValues", "Category Values" );
CAF_PDM_InitScriptableFieldNoDefault( &m_categoryNames, "CategoryNames", "Category Names" );
CAF_PDM_InitScriptableFieldNoDefault( &m_colors, "Colors", "Colors" );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::expected<caf::PdmObjectHandle*, QString> RimcColorLegendCollection_updateColorLegend::execute()
{
auto collection = self<RimColorLegendCollection>();
if ( !collection ) return std::unexpected( "No color legend collection found" );
if ( !m_case() ) return std::unexpected( "No case provided" );
if ( m_categoryValues().size() != m_categoryNames().size() )
return std::unexpected( "CategoryValues and CategoryNames must have matching sizes" );
if ( !m_colors().empty() && m_colors().size() != m_categoryValues().size() )
return std::unexpected( "Colors must be empty or match the size of CategoryValues" );
auto legend = collection->updateColorLegend( m_case(), m_resultName(), m_legendName(), m_categoryValues(), m_categoryNames(), m_colors() );
collection->updateConnectedEditors();
return legend;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
QString RimcColorLegendCollection_updateColorLegend::classKeywordReturnedType() const
{
return RimColorLegend::classKeywordStatic();
}
CAF_PDM_OBJECT_METHOD_SOURCE_INIT( RimColorLegendCollection,
RimcColorLegendCollection_setDefaultColorLegendForResult,
"SetDefaultColorLegendForResult" );
@@ -67,6 +67,28 @@ private:
caf::PdmField<cvf::Color3f> m_color;
};
//==================================================================================================
///
//==================================================================================================
class RimcColorLegendCollection_updateColorLegend : public caf::PdmObjectCreationMethod
{
CAF_PDM_HEADER_INIT;
public:
RimcColorLegendCollection_updateColorLegend( caf::PdmObjectHandle* self );
std::expected<caf::PdmObjectHandle*, QString> execute() override;
QString classKeywordReturnedType() const override;
private:
caf::PdmPtrField<RimCase*> m_case;
caf::PdmField<QString> m_resultName;
caf::PdmField<QString> m_legendName;
caf::PdmField<std::vector<int>> m_categoryValues;
caf::PdmField<std::vector<QString>> m_categoryNames;
caf::PdmField<std::vector<cvf::Color3f>> m_colors;
};
//==================================================================================================
///
//==================================================================================================
+22 -17
View File
@@ -3,8 +3,9 @@ Category name/color binding for discrete (INTEGER) grid cell results.
Provides a convenience API on Case for labeling integer result values with
human-readable names and optional colors. Internally, this reuses the existing
ColorLegend infrastructure: a new custom ColorLegend is created and registered
as the default legend for the (case, resultName) pair. When the property is
ColorLegend infrastructure: a custom ColorLegend is registered as the default
legend for the (case, resultName) pair, and repeated calls update that legend
in place so views referencing it keep their binding. When the property is
shown in a 3D view, the legend's item names are used as the category labels.
"""
@@ -55,10 +56,14 @@ def set_discrete_property_category_names(
data_type="INTEGER") or set_active_cell_property(..., data_type="INTEGER")
to display text labels instead of raw integers in the 3D view legend.
Repeated calls for the same property update the existing color legend in
place, so views referencing the legend keep their binding.
Arguments:
property_name (str): Name of the discrete property result.
value_names (Dict[int, str]): Mapping from integer value to label.
An empty dict removes any existing mapping for this property.
Labels must not contain commas. An empty dict removes any
existing mapping for this property.
value_colors (Optional[Dict[int, str]]): Optional per-value colors as
strings accepted by QColor (e.g. "red", "#ff8800"). Values without
a color entry get an auto-assigned palette color.
@@ -66,7 +71,7 @@ def set_discrete_property_category_names(
Defaults to the property name.
Returns:
The created ColorLegend, or None if value_names was empty.
The created or updated ColorLegend, or None if value_names was empty.
"""
project = self.ancestor(Project)
if project is None:
@@ -76,35 +81,35 @@ def set_discrete_property_category_names(
if collection is None:
raise RuntimeError("Could not find ColorLegendCollection in project")
collection.delete_color_legend(case=self, result_name=property_name)
if not value_names:
collection.delete_color_legend(case=self, result_name=property_name)
return None
name = legend_name if legend_name else property_name
legend = collection.create_color_legend(name=name)
category_values = []
category_names = []
category_colors = []
colors = value_colors or {}
palette_index = 0
for value, label in value_names.items():
for value, label in sorted(value_names.items()):
color = colors.get(value)
if color is None:
color = _DEFAULT_PALETTE[palette_index % len(_DEFAULT_PALETTE)]
palette_index += 1
legend.add_color_legend_item(
category_value=value,
category_name=label,
color=color,
)
category_values.append(value)
category_names.append(label)
category_colors.append(color)
collection.set_default_color_legend_for_result(
return collection.update_color_legend(
case=self,
result_name=property_name,
color_legend=legend,
legend_name=name,
category_values=category_values,
category_names=category_names,
colors=category_colors,
)
return legend
def _find_default_legend(case: Case, property_name: str) -> Optional[ColorLegend]:
"""Look up the color legend bound to (case, property_name).
@@ -130,3 +130,43 @@ def test_discrete_property_category_no_duplicate_legend(rips_instance, initializ
1: "Shale",
2: "Coal",
}
def test_discrete_property_category_update_preserves_legend_identity(
rips_instance, initialize_test
):
case_path = dataroot.PATH + "/TEST10K_FLT_LGR_NNC/TEST10K_FLT_LGR_NNC.EGRID"
case = rips_instance.project.load_case(path=case_path)
assert case is not None
collection = rips_instance.project.color_legend_collection()
def legend_count():
return len(collection.descendants(rips.ColorLegend))
legend1 = case.set_discrete_property_category_names(
property_name="FACIES", value_names={0: "Sand", 1: "Shale"}
)
assert legend1 is not None
after_first = legend_count()
# A repeated call must update the existing legend object in place so that
# views referencing it keep their binding.
new_names = {0: "Sandstone", 1: "Shale", 2: "Coal"}
new_colors = {0: "#e6c878", 1: "#646464", 2: "#202020"}
legend2 = case.set_discrete_property_category_names(
property_name="FACIES", value_names=new_names, value_colors=new_colors
)
assert legend2.address() == legend1.address()
assert legend_count() == after_first
# The items are fully replaced, with no leftovers from the first call.
assert case.discrete_property_category_names("FACIES") == new_names
assert case.discrete_property_category_colors("FACIES") == new_colors
# The legend can be renamed in place as well.
legend3 = case.set_discrete_property_category_names(
property_name="FACIES", value_names=new_names, legend_name="My Facies"
)
assert legend3.address() == legend1.address()
assert legend3.color_legend_name == "My Facies"