From f9e7c4bf361cc38c8be4e5c5d4147a976f7b4fce Mon Sep 17 00:00:00 2001 From: Magne Sjaastad Date: Mon, 19 Oct 2026 09:40:46 +0200 Subject: [PATCH 1/4] #12925: Improve caf editors, label links and nested settings write --- .../cafPdmUiCore/cafPdmUiLabel.cpp | 16 +++ .../cafPdmUiCore/cafPdmUiLabel.h | 9 ++ .../cafPdmXml/cafPdmSettings.cpp | 11 +-- .../cafPdmXml_UnitTests/CMakeLists.txt | 8 +- .../cafPdmSettingsTest.cpp | 94 ++++++++++++++++++ .../cafPdmUiFormLayoutObjectEditor.cpp | 15 +++ .../cafUserInterface/cafPdmUiListEditor.cpp | 32 +++++- .../cafUserInterface/cafPdmUiListEditor.h | 4 +- .../cafUserInterface_UnitTests/CMakeLists.txt | 1 + .../cafPdmUiFormLayoutObjectEditorTest.cpp | 97 +++++++++++++++++++ 10 files changed, 276 insertions(+), 11 deletions(-) create mode 100644 Fwk/AppFwk/cafProjectDataModel/cafPdmXml/cafPdmXml_UnitTests/cafPdmSettingsTest.cpp create mode 100644 Fwk/AppFwk/cafUserInterface/cafUserInterface_UnitTests/cafPdmUiFormLayoutObjectEditorTest.cpp diff --git a/Fwk/AppFwk/cafProjectDataModel/cafPdmUiCore/cafPdmUiLabel.cpp b/Fwk/AppFwk/cafProjectDataModel/cafPdmUiCore/cafPdmUiLabel.cpp index 352a12e91a0..59458ed17b0 100644 --- a/Fwk/AppFwk/cafProjectDataModel/cafPdmUiCore/cafPdmUiLabel.cpp +++ b/Fwk/AppFwk/cafProjectDataModel/cafPdmUiCore/cafPdmUiLabel.cpp @@ -62,4 +62,20 @@ bool PdmUiLabel::isUiGroup() const return false; } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void PdmUiLabel::setLinkActivatedCallback( const std::function& callback ) +{ + m_linkActivatedCallback = callback; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +const std::function& PdmUiLabel::linkActivatedCallback() const +{ + return m_linkActivatedCallback; +} + } // End of namespace caf diff --git a/Fwk/AppFwk/cafProjectDataModel/cafPdmUiCore/cafPdmUiLabel.h b/Fwk/AppFwk/cafProjectDataModel/cafPdmUiCore/cafPdmUiLabel.h index 2969ef1522e..b9db90d6a1d 100644 --- a/Fwk/AppFwk/cafProjectDataModel/cafPdmUiCore/cafPdmUiLabel.h +++ b/Fwk/AppFwk/cafProjectDataModel/cafPdmUiCore/cafPdmUiLabel.h @@ -38,6 +38,8 @@ #include "cafPdmUiItem.h" +#include + namespace caf { //================================================================================================== @@ -51,6 +53,13 @@ class PdmUiLabel : public PdmUiItem explicit PdmUiLabel( const QString& labelText ); bool isUiGroup() const override; + + // Called with the href when a hyperlink in a rich text label is clicked + void setLinkActivatedCallback( const std::function& callback ); + const std::function& linkActivatedCallback() const; + +private: + std::function m_linkActivatedCallback; }; } // End of namespace caf diff --git a/Fwk/AppFwk/cafProjectDataModel/cafPdmXml/cafPdmSettings.cpp b/Fwk/AppFwk/cafProjectDataModel/cafPdmXml/cafPdmSettings.cpp index 6f66ab2a915..60a70b22fd2 100644 --- a/Fwk/AppFwk/cafProjectDataModel/cafPdmXml/cafPdmSettings.cpp +++ b/Fwk/AppFwk/cafProjectDataModel/cafPdmXml/cafPdmSettings.cpp @@ -117,14 +117,11 @@ void PdmSettings::writeFieldsToApplicationStore( const caf::PdmObjectHandle* obj caf::PdmObjectHandle* child = children[childIdx]; if ( !child ) continue; - QString subContext; - if ( context.isEmpty() ) - { - caf::PdmXmlObjectHandle* xmlObjHandle = xmlObj( child ); - if ( !xmlObjHandle ) continue; + caf::PdmXmlObjectHandle* xmlObjHandle = xmlObj( child ); + if ( !xmlObjHandle ) continue; - subContext = xmlObjHandle->classKeyword() + "/"; - } + // Use the same nested context as when reading, to support children of children + const QString subContext = context + xmlObjHandle->classKeyword() + "/"; writeFieldsToApplicationStore( child, subContext ); } diff --git a/Fwk/AppFwk/cafProjectDataModel/cafPdmXml/cafPdmXml_UnitTests/CMakeLists.txt b/Fwk/AppFwk/cafProjectDataModel/cafPdmXml/cafPdmXml_UnitTests/CMakeLists.txt index 43a04c2f034..6d30ee54e94 100644 --- a/Fwk/AppFwk/cafProjectDataModel/cafPdmXml/cafPdmXml_UnitTests/CMakeLists.txt +++ b/Fwk/AppFwk/cafProjectDataModel/cafPdmXml/cafPdmXml_UnitTests/CMakeLists.txt @@ -20,9 +20,13 @@ include_directories(${CMAKE_CURRENT_SOURCE_DIR} # required for gtest-all.cpp ) set(PROJECT_FILES - cafPdmXml_UnitTests.cpp gtest/gtest-all.cpp cafPdmXmlBasicTest.cpp - cafPdmAdvancedTemplateTest.cpp cafPdmXmlNumberTest.cpp + cafPdmXml_UnitTests.cpp + gtest/gtest-all.cpp + cafPdmXmlBasicTest.cpp + cafPdmAdvancedTemplateTest.cpp + cafPdmXmlNumberTest.cpp cafPdmPtrArrayTest.cpp + cafPdmSettingsTest.cpp ) qt_add_executable(${PROJECT_NAME} MACOSX_BUNDLE ${PROJECT_FILES}) diff --git a/Fwk/AppFwk/cafProjectDataModel/cafPdmXml/cafPdmXml_UnitTests/cafPdmSettingsTest.cpp b/Fwk/AppFwk/cafProjectDataModel/cafPdmXml/cafPdmXml_UnitTests/cafPdmSettingsTest.cpp new file mode 100644 index 00000000000..5fa1227c409 --- /dev/null +++ b/Fwk/AppFwk/cafProjectDataModel/cafPdmXml/cafPdmXml_UnitTests/cafPdmSettingsTest.cpp @@ -0,0 +1,94 @@ +#include "gtest/gtest.h" + +#include "cafPdmChildField.h" +#include "cafPdmDataValueField.h" +#include "cafPdmObjectHandle.h" +#include "cafPdmSettings.h" +#include "cafPdmXmlObjectHandle.h" +#include "cafPdmXmlObjectHandleMacros.h" + +#include +#include +#include + +class SettingsLeaf : public caf::PdmObjectHandle, public caf::PdmXmlObjectHandle +{ + CAF_PDM_XML_HEADER_INIT; + +public: + SettingsLeaf() + : PdmObjectHandle() + , PdmXmlObjectHandle( this, false ) + { + CAF_PDM_XML_InitField( &m_value, "Value" ); + } + + caf::PdmDataValueField m_value; +}; +CAF_PDM_XML_SOURCE_INIT( SettingsLeaf, "SettingsLeaf" ); + +class SettingsMiddle : public caf::PdmObjectHandle, public caf::PdmXmlObjectHandle +{ + CAF_PDM_XML_HEADER_INIT; + +public: + SettingsMiddle() + : PdmObjectHandle() + , PdmXmlObjectHandle( this, false ) + { + CAF_PDM_XML_InitField( &m_leaf, "Leaf" ); + m_leaf = new SettingsLeaf; + } + + ~SettingsMiddle() { delete m_leaf(); } + + caf::PdmChildField m_leaf; +}; +CAF_PDM_XML_SOURCE_INIT( SettingsMiddle, "SettingsMiddle" ); + +class SettingsRoot : public caf::PdmObjectHandle, public caf::PdmXmlObjectHandle +{ + CAF_PDM_XML_HEADER_INIT; + +public: + SettingsRoot() + : PdmObjectHandle() + , PdmXmlObjectHandle( this, false ) + { + CAF_PDM_XML_InitField( &m_middle, "Middle" ); + m_middle = new SettingsMiddle; + } + + ~SettingsRoot() { delete m_middle(); } + + caf::PdmChildField m_middle; +}; +CAF_PDM_XML_SOURCE_INIT( SettingsRoot, "SettingsRoot" ); + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +TEST( PdmSettings, NestedChildrenRoundTrip ) +{ + QSettings::setDefaultFormat( QSettings::IniFormat ); + QSettings::setPath( QSettings::IniFormat, QSettings::UserScope, QDir::tempPath() ); + QCoreApplication::setOrganizationName( "cafPdmSettingsTest" ); + QCoreApplication::setApplicationName( "cafPdmSettingsTest" ); + QSettings().clear(); + + { + SettingsRoot root; + root.m_middle()->m_leaf()->m_value = 42; + caf::PdmSettings::writeFieldsToApplicationStore( &root ); + } + + // Written with the same nested context as used when reading + EXPECT_TRUE( QSettings().contains( "SettingsMiddle/SettingsLeaf/Value" ) ); + + SettingsRoot restored; + restored.m_middle()->m_leaf()->m_value = 0; + caf::PdmSettings::readFieldsFromApplicationStore( &restored ); + EXPECT_EQ( 42, restored.m_middle()->m_leaf()->m_value() ); + + QSettings().clear(); +} diff --git a/Fwk/AppFwk/cafUserInterface/cafPdmUiFormLayoutObjectEditor.cpp b/Fwk/AppFwk/cafUserInterface/cafPdmUiFormLayoutObjectEditor.cpp index 91b27243eea..5f12f642c0a 100644 --- a/Fwk/AppFwk/cafUserInterface/cafPdmUiFormLayoutObjectEditor.cpp +++ b/Fwk/AppFwk/cafUserInterface/cafPdmUiFormLayoutObjectEditor.cpp @@ -488,6 +488,13 @@ QLabel* caf::PdmUiFormLayoutObjectEditor::createLabel( QWidget* parent, const Pd qLabel->setText( label.uiName( uiConfigName ) ); qLabel->setWordWrap( true ); + if ( auto callback = label.linkActivatedCallback() ) + { + qLabel->setTextFormat( Qt::RichText ); + qLabel->setTextInteractionFlags( Qt::LinksAccessibleByMouse ); + QObject::connect( qLabel, &QLabel::linkActivated, qLabel, [callback]( const QString& link ) { callback( link ); } ); + } + m_labels.push_back( qLabel ); return qLabel; } @@ -562,6 +569,14 @@ caf::PdmUiFieldEditorHandle* caf::PdmUiFormLayoutObjectEditor::findOrCreateField std::map::iterator it = m_fieldViews.find( field->fieldHandle() ); + // The editor's field was deleted and a new field was allocated at the same address + if ( it != m_fieldViews.end() && it->second->uiField() != field ) + { + delete it->second; + m_fieldViews.erase( it ); + it = m_fieldViews.end(); + } + if ( it == m_fieldViews.end() ) { fieldEditor = PdmUiFieldEditorHelper::createFieldEditorForField( field, uiConfigName ); diff --git a/Fwk/AppFwk/cafUserInterface/cafPdmUiListEditor.cpp b/Fwk/AppFwk/cafUserInterface/cafPdmUiListEditor.cpp index d9bc03cc8c0..0cdebb289de 100644 --- a/Fwk/AppFwk/cafUserInterface/cafPdmUiListEditor.cpp +++ b/Fwk/AppFwk/cafUserInterface/cafPdmUiListEditor.cpp @@ -76,8 +76,24 @@ class MyStringListModel : public QStringListModel void setItemsEditable( bool isEditable ) { m_isItemsEditable = isEditable; } + void setIcons( const QList& icons ) { m_icons = icons; } + + QVariant data( const QModelIndex& index, int role ) const override + { + if ( role == Qt::DecorationRole ) + { + if ( index.isValid() && index.row() < m_icons.size() && !m_icons[index.row()].isNull() ) + { + return m_icons[index.row()]; + } + return QVariant(); + } + return QStringListModel::data( index, role ); + } + private: - bool m_isItemsEditable; + bool m_isItemsEditable; + QList m_icons; }; //================================================================================================== @@ -194,6 +210,19 @@ void PdmUiListEditor::configureAndUpdateUi( const QString& uiConfigName ) strListModel->setItemsEditable( false ); QModelIndex currentItem = m_listView->selectionModel()->currentIndex(); QStringList texts = PdmOptionItemInfo::extractUiTexts( options ); + + QList icons; + for ( const auto& option : options ) + { + auto icon = option.icon(); + icons.push_back( icon ? *icon : QIcon() ); + } + strListModel->setIcons( icons ); + + if ( auto iconSize = uiField()->attribute( Keys::ICON_SIZE, uiConfigName ) ) + { + m_listView->setIconSize( *iconSize ); + } strListModel->setStringList( texts ); QVariant fieldValue = uiField()->uiValue(); @@ -247,6 +276,7 @@ void PdmUiListEditor::configureAndUpdateUi( const QString& uiConfigName ) QVariant fieldValue = uiField()->uiValue(); QStringList texts = fieldValue.toStringList(); texts.push_back( "" ); + strListModel->setIcons( {} ); strListModel->setStringList( texts ); strListModel->setItemsEditable( true ); diff --git a/Fwk/AppFwk/cafUserInterface/cafPdmUiListEditor.h b/Fwk/AppFwk/cafUserInterface/cafPdmUiListEditor.h index 1f1e4875162..d2248fc09e0 100644 --- a/Fwk/AppFwk/cafUserInterface/cafPdmUiListEditor.h +++ b/Fwk/AppFwk/cafUserInterface/cafPdmUiListEditor.h @@ -66,13 +66,15 @@ class PdmUiListEditor : public PdmUiFieldLabelEditorHandle static inline const QString BASE_COLOR = QStringLiteral( "baseColor" ); static inline const QString QSS_STATE = QStringLiteral( "qssState" ); static inline const QString ALLOW_HORIZONTAL_SCROLL_BAR = QStringLiteral( "allowHorizontalScrollBar" ); + static inline const QString ICON_SIZE = QStringLiteral( "iconSize" ); // QSize }; // Set of all supported attributes for validation inline static const std::set SUPPORTED_ATTRIBUTES = { Keys::HEIGHT_HINT, Keys::BASE_COLOR, Keys::QSS_STATE, - Keys::ALLOW_HORIZONTAL_SCROLL_BAR }; + Keys::ALLOW_HORIZONTAL_SCROLL_BAR, + Keys::ICON_SIZE }; protected: QWidget* createEditorWidget( QWidget* parent ) override; diff --git a/Fwk/AppFwk/cafUserInterface/cafUserInterface_UnitTests/CMakeLists.txt b/Fwk/AppFwk/cafUserInterface/cafUserInterface_UnitTests/CMakeLists.txt index 0036550edd4..061d1497c58 100644 --- a/Fwk/AppFwk/cafUserInterface/cafUserInterface_UnitTests/CMakeLists.txt +++ b/Fwk/AppFwk/cafUserInterface/cafUserInterface_UnitTests/CMakeLists.txt @@ -25,6 +25,7 @@ set(PROJECT_FILES cafPdmUiCheckBoxAndTextEditorTest.cpp cafPdmUiPropertyViewDialogTest.cpp cafPdmUiEditorHandleTest.cpp + cafPdmUiFormLayoutObjectEditorTest.cpp cafIconProviderTest.cpp cafProgressInfoTest.cpp gtest/gtest-all.cpp diff --git a/Fwk/AppFwk/cafUserInterface/cafUserInterface_UnitTests/cafPdmUiFormLayoutObjectEditorTest.cpp b/Fwk/AppFwk/cafUserInterface/cafUserInterface_UnitTests/cafPdmUiFormLayoutObjectEditorTest.cpp new file mode 100644 index 00000000000..726c0d309b1 --- /dev/null +++ b/Fwk/AppFwk/cafUserInterface/cafUserInterface_UnitTests/cafPdmUiFormLayoutObjectEditorTest.cpp @@ -0,0 +1,97 @@ +#include "gtest/gtest.h" + +#include "cafPdmChildField.h" +#include "cafPdmField.h" +#include "cafPdmObject.h" +#include "cafPdmUiOrdering.h" +#include "cafPdmUiPropertyView.h" + +#include + +using namespace caf; + +namespace +{ +alignas( std::max_align_t ) unsigned char childStorage[4096]; +} // namespace + +//-------------------------------------------------------------------------------------------------- +/// Always allocated at the same address, to reproduce a deleted object being replaced by one at the same address +//-------------------------------------------------------------------------------------------------- +class FormLayoutTestChild : public caf::PdmObject +{ + CAF_PDM_HEADER_INIT; + +public: + FormLayoutTestChild() + { + CAF_PDM_InitObject( "FormLayoutTestChild" ); + CAF_PDM_InitField( &m_text, "Text", QString( "text" ), "Text" ); + } + + static void* operator new( size_t size ) + { + EXPECT_LE( size, sizeof( childStorage ) ); + return childStorage; + } + static void operator delete( void* ) {} + + caf::PdmField m_text; +}; +CAF_PDM_SOURCE_INIT( FormLayoutTestChild, "FormLayoutTestChild" ); + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +class FormLayoutTestParent : public caf::PdmObject +{ + CAF_PDM_HEADER_INIT; + +public: + FormLayoutTestParent() + { + CAF_PDM_InitObject( "FormLayoutTestParent" ); + CAF_PDM_InitFieldNoDefault( &m_child, "Child", "Child" ); + m_child = new FormLayoutTestChild; + } + + void replaceChild() + { + delete m_child(); + m_child = new FormLayoutTestChild; + } + + caf::PdmChildField m_child; + +protected: + void defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) override + { + uiOrdering.add( &m_child->m_text ); + uiOrdering.skipRemainingFields(); + } +}; +CAF_PDM_SOURCE_INIT( FormLayoutTestParent, "FormLayoutTestParent" ); + +//-------------------------------------------------------------------------------------------------- +/// A field editor bound to a deleted field must not be reused for a new field at the same address +//-------------------------------------------------------------------------------------------------- +TEST( PdmUiFormLayoutObjectEditorTest, FieldReplacedAtSameAddress ) +{ + FormLayoutTestParent parent; + + PdmUiPropertyView view; + view.showProperties( &parent ); + + auto oldField = &parent.m_child->m_text; + ASSERT_EQ( 1u, oldField->uiCapability()->connectedEditors().size() ); + + parent.replaceChild(); + ASSERT_EQ( oldField, &parent.m_child->m_text ); + ASSERT_TRUE( parent.m_child->m_text.uiCapability()->connectedEditors().empty() ); + + parent.updateConnectedEditors(); + + EXPECT_EQ( 1u, parent.m_child->m_text.uiCapability()->connectedEditors().size() ); + + view.showProperties( nullptr ); +} From 17fa1d699a5ece383ff06293b4d38788d79095d0 Mon Sep 17 00:00:00 2001 From: Magne Sjaastad Date: Mon, 19 Oct 2026 09:40:46 +0200 Subject: [PATCH 2/4] #12925: Add global curve style settings and per-item rules --- .../Tools/Summary/RiaSummaryTools.cpp | 41 ++ .../Tools/Summary/RiaSummaryTools.h | 6 +- .../ProjectDataModel/RimPlotCurve.h | 12 +- .../RimPlotCurveAppearance.cpp | 60 +- .../ProjectDataModel/RimPlotCurveAppearance.h | 9 +- .../ProjectDataModel/RimProject.cpp | 12 + .../ProjectDataModel/RimProject.h | 3 + .../Summary/CMakeLists_files.cmake | 4 + .../Summary/RimCurveStyleRule.cpp | 575 +++++++++++++++ .../Summary/RimCurveStyleRule.h | 142 ++++ .../Summary/RimCurveStyleRules.cpp | 659 ++++++++++++++++++ .../Summary/RimCurveStyleRules.h | 137 ++++ .../Summary/RimCurveStyleSettings.cpp | 197 ++++++ .../Summary/RimCurveStyleSettings.h | 79 +++ .../Summary/RimSavedCurveAppearance.cpp | 76 ++ .../Summary/RimSavedCurveAppearance.h | 53 ++ ApplicationLibCode/UnitTests/CMakeLists.txt | 2 + .../UnitTests/RimCurveStyleRules-Test.cpp | 124 ++++ .../UnitTests/RimCurveStyleSettings-Test.cpp | 32 + 19 files changed, 2215 insertions(+), 8 deletions(-) create mode 100644 ApplicationLibCode/ProjectDataModel/Summary/RimCurveStyleRule.cpp create mode 100644 ApplicationLibCode/ProjectDataModel/Summary/RimCurveStyleRule.h create mode 100644 ApplicationLibCode/ProjectDataModel/Summary/RimCurveStyleRules.cpp create mode 100644 ApplicationLibCode/ProjectDataModel/Summary/RimCurveStyleRules.h create mode 100644 ApplicationLibCode/ProjectDataModel/Summary/RimCurveStyleSettings.cpp create mode 100644 ApplicationLibCode/ProjectDataModel/Summary/RimCurveStyleSettings.h create mode 100644 ApplicationLibCode/ProjectDataModel/Summary/RimSavedCurveAppearance.cpp create mode 100644 ApplicationLibCode/ProjectDataModel/Summary/RimSavedCurveAppearance.h create mode 100644 ApplicationLibCode/UnitTests/RimCurveStyleRules-Test.cpp create mode 100644 ApplicationLibCode/UnitTests/RimCurveStyleSettings-Test.cpp diff --git a/ApplicationLibCode/Application/Tools/Summary/RiaSummaryTools.cpp b/ApplicationLibCode/Application/Tools/Summary/RiaSummaryTools.cpp index a1f705223e7..d650dd21ed3 100644 --- a/ApplicationLibCode/Application/Tools/Summary/RiaSummaryTools.cpp +++ b/ApplicationLibCode/Application/Tools/Summary/RiaSummaryTools.cpp @@ -68,6 +68,47 @@ std::vector RiaSummaryTools::singleTopLevelSummaryCases() return {}; } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::set RiaSummaryTools::allSummaryCaseNames() +{ + std::set summaryCaseFilenames; + + for ( RimSummaryCase* rimCase : RimProject::current()->allSummaryCases() ) + { + summaryCaseFilenames.insert( rimCase->summaryHeaderFilename().toUtf8().constData() ); + } + + return summaryCaseFilenames; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::set RiaSummaryTools::allSummaryWellNames() +{ + std::set summaryWellNames; + + for ( RimSummaryCase* rimCase : RimProject::current()->allSummaryCases() ) + { + if ( !rimCase ) continue; + + RifSummaryReaderInterface* reader = rimCase->summaryReader(); + if ( !reader ) continue; + + for ( const auto& address : reader->allResultAddresses() ) + { + if ( address.category() == RifEclipseSummaryAddressDefines::SummaryCategory::SUMMARY_WELL ) + { + summaryWellNames.insert( address.wellName() ); + } + } + } + + return summaryWellNames; +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/Application/Tools/Summary/RiaSummaryTools.h b/ApplicationLibCode/Application/Tools/Summary/RiaSummaryTools.h index 6c18b0d495b..fe0c0c1d1d2 100644 --- a/ApplicationLibCode/Application/Tools/Summary/RiaSummaryTools.h +++ b/ApplicationLibCode/Application/Tools/Summary/RiaSummaryTools.h @@ -27,6 +27,8 @@ #include #include +#include +#include #include class RifEclipseSummaryAddress; @@ -60,7 +62,9 @@ class RiaSummaryTools static std::vector singleTopLevelSummaryCases(); - static void notifyCalculatedCurveNameHasChanged( int calculationId, const QString& currentCurveName ); + static std::set allSummaryCaseNames(); + static std::set allSummaryWellNames(); + static void notifyCalculatedCurveNameHasChanged( int calculationId, const QString& currentCurveName ); static RimSummaryPlot* parentSummaryPlot( caf::PdmObject* object ); static RimSummaryMultiPlot* parentSummaryMultiPlot( caf::PdmObject* object ); diff --git a/ApplicationLibCode/ProjectDataModel/RimPlotCurve.h b/ApplicationLibCode/ProjectDataModel/RimPlotCurve.h index 06f6975cda4..5fd5880779a 100644 --- a/ApplicationLibCode/ProjectDataModel/RimPlotCurve.h +++ b/ApplicationLibCode/ProjectDataModel/RimPlotCurve.h @@ -75,11 +75,13 @@ class RimPlotCurve : public caf::PdmObject void setSymbolSize( int sizeInPixels ); void setLineThickness( int thickness ); void resetAppearance(); - virtual Qt::BrushStyle fillStyle() const; - void setFillStyle( Qt::BrushStyle brushStyle ); - void setFillColor( const cvf::Color3f& fillColor ); - void setFillColorOpacity( float opacity ); - void setCurveColorOpacity( float opacity ); + + RimPlotCurveAppearance* curveAppearance() const { return m_curveAppearance(); } + virtual Qt::BrushStyle fillStyle() const; + void setFillStyle( Qt::BrushStyle brushStyle ); + void setFillColor( const cvf::Color3f& fillColor ); + void setFillColorOpacity( float opacity ); + void setCurveColorOpacity( float opacity ); bool isChecked() const; void setCheckState( bool isChecked ); diff --git a/ApplicationLibCode/ProjectDataModel/RimPlotCurveAppearance.cpp b/ApplicationLibCode/ProjectDataModel/RimPlotCurveAppearance.cpp index c6a8dc1fc17..128e756e5ad 100644 --- a/ApplicationLibCode/ProjectDataModel/RimPlotCurveAppearance.cpp +++ b/ApplicationLibCode/ProjectDataModel/RimPlotCurveAppearance.cpp @@ -24,7 +24,7 @@ #include "cafPdmUiCommandSystemProxy.h" #include "cafPdmUiDoubleSliderEditor.h" -CAF_PDM_XML_ABSTRACT_SOURCE_INIT( RimPlotCurveAppearance, "PlotCurveAppearance" ); +CAF_PDM_XML_SOURCE_INIT( RimPlotCurveAppearance, "PlotCurveAppearance" ); namespace caf { @@ -236,9 +236,43 @@ void RimPlotCurveAppearance::defineUiOrdering( QString uiConfigName, caf::PdmUiO uiOrdering.add( &m_curveInterpolation ); m_curveInterpolation.uiCapability()->setUiHidden( !m_interpolationVisible ); + for ( auto field : std::vector{ &m_curveColor, + &m_curveColorOpacity, + &m_pointSymbol, + &m_symbolEdgeColor, + &m_symbolSize, + &m_symbolSkipPixelDistance, + &m_lineStyle, + &m_curveThickness, + &m_fillStyle, + &m_fillColor, + &m_fillColorOpacity, + &m_curveInterpolation } ) + { + field->uiCapability()->setUiReadOnly( m_uiLocked ); + } + + // Keep the fields that are irrelevant for the current symbol and line style read-only + const bool noSymbol = m_pointSymbol() == RiuPlotCurveSymbol::SYMBOL_NONE; + const bool noLine = m_lineStyle() == RiuQwtPlotCurveDefines::LineStyleEnum::STYLE_NONE; + m_symbolSize.uiCapability()->setUiReadOnly( m_uiLocked || noSymbol ); + m_symbolSkipPixelDistance.uiCapability()->setUiReadOnly( m_uiLocked || noSymbol ); + m_curveThickness.uiCapability()->setUiReadOnly( m_uiLocked || noLine ); + m_curveInterpolation.uiCapability()->setUiReadOnly( m_uiLocked || noLine ); + + if ( m_curveFittingToleranceVisible && m_uiLocked ) m_curveFittingTolerance.uiCapability()->setUiReadOnly( true ); + uiOrdering.skipRemainingFields(); } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimPlotCurveAppearance::setUiLocked( bool isLocked ) +{ + m_uiLocked = isLocked; +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -401,6 +435,30 @@ int RimPlotCurveAppearance::lineThickness() const return m_curveThickness; } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimPlotCurveAppearance::copyFrom( const RimPlotCurveAppearance& other ) +{ + // Fields are assigned directly, as this runs per curve on every restyle. XML based copies like copyObject() or + // readObjectFromXmlString( other.writeObjectToXmlString() ) are too slow for many curves. New fields must be added here. + m_symbolLabel = other.m_symbolLabel(); + m_symbolSize = other.m_symbolSize(); + m_curveColor = other.m_curveColor(); + m_curveColorOpacity = other.m_curveColorOpacity(); + m_curveThickness = other.m_curveThickness(); + m_symbolSkipPixelDistance = other.m_symbolSkipPixelDistance(); + m_curveFittingTolerance = other.m_curveFittingTolerance(); + m_pointSymbol = other.m_pointSymbol(); + m_lineStyle = other.m_lineStyle(); + m_fillStyle = other.m_fillStyle(); + m_fillColor = other.m_fillColor(); + m_fillColorOpacity = other.m_fillColorOpacity(); + m_curveInterpolation = other.m_curveInterpolation(); + m_symbolLabelPosition = other.m_symbolLabelPosition(); + m_symbolEdgeColor = other.m_symbolEdgeColor(); +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/ProjectDataModel/RimPlotCurveAppearance.h b/ApplicationLibCode/ProjectDataModel/RimPlotCurveAppearance.h index 2316a756c92..22d54796d70 100644 --- a/ApplicationLibCode/ProjectDataModel/RimPlotCurveAppearance.h +++ b/ApplicationLibCode/ProjectDataModel/RimPlotCurveAppearance.h @@ -79,7 +79,10 @@ class RimPlotCurveAppearance : public caf::PdmObject void setSymbolLabelPosition( RiuPlotCurveSymbol::LabelPosition labelPosition ); RiuPlotCurveSymbol::LabelPosition symbolLabelPosition() const; - void resetAppearance(); + void resetAppearance(); + + // Copies the visual settings from another appearance without emitting signals + void copyFrom( const RimPlotCurveAppearance& other ); Qt::BrushStyle fillStyle() const; void setFillStyle( Qt::BrushStyle brushStyle ); @@ -102,6 +105,9 @@ class RimPlotCurveAppearance : public caf::PdmObject void setFillOptionsVisible( bool isVisible ); void setCurveFittingToleranceVisible( bool isVisible ); + // Shows all fields as read-only, used when the appearance is controlled elsewhere + void setUiLocked( bool isLocked ); + void defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) override; protected: @@ -135,4 +141,5 @@ class RimPlotCurveAppearance : public caf::PdmObject bool m_interpolationVisible; bool m_fillOptionsVisible; bool m_curveFittingToleranceVisible; + bool m_uiLocked = false; }; diff --git a/ApplicationLibCode/ProjectDataModel/RimProject.cpp b/ApplicationLibCode/ProjectDataModel/RimProject.cpp index 977d610b67b..7ea25ff8778 100644 --- a/ApplicationLibCode/ProjectDataModel/RimProject.cpp +++ b/ApplicationLibCode/ProjectDataModel/RimProject.cpp @@ -101,6 +101,7 @@ #include "RimWellPath.h" #include "RimWellPathCollection.h" #include "RimWellTargetMapping.h" +#include "Summary/RimCurveStyleRules.h" #include "Tools/RimAutomationSettings.h" #include "VerticalFlowPerformance/RimVfpDataCollection.h" #include "VerticalFlowPerformance/RimVfpPlotCollection.h" @@ -148,6 +149,9 @@ RimProject::RimProject() CAF_PDM_InitFieldNoDefault( &m_mainPlotCollection, "MainPlotCollection", "Plots" ); CAF_PDM_InitFieldNoDefault( &m_pinnedFieldCollection, "PinnedFieldCollection", "PinnedFieldCollection" ); + CAF_PDM_InitFieldNoDefault( &m_curveStyleRules, "CurveStyleRules", "Curve Style Rules" ); + m_curveStyleRules = new RimCurveStyleRules; + CAF_PDM_InitFieldNoDefault( &viewLinkerCollection, "LinkedViews", "Linked Views", ":/LinkView.svg" ); viewLinkerCollection = new RimViewLinkerCollection; @@ -346,6 +350,14 @@ RimQuickAccessCollection* RimProject::pinnedFieldCollection() const return m_pinnedFieldCollection(); } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimCurveStyleRules* RimProject::curveStyleRules() const +{ + return m_curveStyleRules(); +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/ProjectDataModel/RimProject.h b/ApplicationLibCode/ProjectDataModel/RimProject.h index 095ccdb6ee6..6a5d8f0c477 100644 --- a/ApplicationLibCode/ProjectDataModel/RimProject.h +++ b/ApplicationLibCode/ProjectDataModel/RimProject.h @@ -71,6 +71,7 @@ class RimCompletionTemplateCollection; class RimPlotTemplateFolderItem; class RimGridCalculationCollection; class RimQuickAccessCollection; +class RimCurveStyleRules; class RimAutomationSettings; class RimJobCollection; class RimEnsembleFileSetCollection; @@ -194,6 +195,7 @@ class RimProject : public caf::PdmDocument void updatesAfterProjectFileIsRead(); RimQuickAccessCollection* pinnedFieldCollection() const; + RimCurveStyleRules* curveStyleRules() const; RimAutomationSettings* automationSettings() const; RimEnsembleFileSetCollection* ensembleFileSetCollection() const; RimJobCollection* jobCollection() const; @@ -208,6 +210,7 @@ class RimProject : public caf::PdmDocument private: caf::PdmChildField m_mainPlotCollection; caf::PdmChildField m_pinnedFieldCollection; + caf::PdmChildField m_curveStyleRules; caf::PdmChildField m_automationSettings; caf::PdmChildField m_ensembleFileSetCollection; caf::PdmChildField m_jobCollection; diff --git a/ApplicationLibCode/ProjectDataModel/Summary/CMakeLists_files.cmake b/ApplicationLibCode/ProjectDataModel/Summary/CMakeLists_files.cmake index 7cafead3b47..0d03ab04d47 100644 --- a/ApplicationLibCode/ProjectDataModel/Summary/CMakeLists_files.cmake +++ b/ApplicationLibCode/ProjectDataModel/Summary/CMakeLists_files.cmake @@ -8,6 +8,10 @@ set(SOURCE_GROUP_SOURCE_FILES ${CMAKE_CURRENT_LIST_DIR}/RimSummaryCurve.cpp ${CMAKE_CURRENT_LIST_DIR}/RimSummaryCurvesData.cpp ${CMAKE_CURRENT_LIST_DIR}/RimSummaryCurveAppearanceCalculator.cpp + ${CMAKE_CURRENT_LIST_DIR}/RimCurveStyleSettings.cpp + ${CMAKE_CURRENT_LIST_DIR}/RimCurveStyleRule.cpp + ${CMAKE_CURRENT_LIST_DIR}/RimCurveStyleRules.cpp + ${CMAKE_CURRENT_LIST_DIR}/RimSavedCurveAppearance.cpp ${CMAKE_CURRENT_LIST_DIR}/RimSummaryCurveAutoName.cpp ${CMAKE_CURRENT_LIST_DIR}/RimSummaryPlotAxisFormatter.cpp ${CMAKE_CURRENT_LIST_DIR}/RimSummaryCurveCollection.cpp diff --git a/ApplicationLibCode/ProjectDataModel/Summary/RimCurveStyleRule.cpp b/ApplicationLibCode/ProjectDataModel/Summary/RimCurveStyleRule.cpp new file mode 100644 index 00000000000..6687e0f8ff5 --- /dev/null +++ b/ApplicationLibCode/ProjectDataModel/Summary/RimCurveStyleRule.cpp @@ -0,0 +1,575 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#include "RimCurveStyleRule.h" + +#include "RiaColorTools.h" +#include "Summary/RiaSummaryTools.h" + +#include "RimCurveStyleRules.h" +#include "RimEnsembleCurveSet.h" +#include "RimEnsembleCurveSetCollection.h" +#include "RimPlotCurve.h" +#include "RimProject.h" +#include "RimSummaryCase.h" +#include "RimSummaryCurve.h" +#include "RimSummaryEnsemble.h" +#include "RimSummaryPlot.h" + +#include "RiuPlotCurveSymbolImageCreator.h" + +#include +#include +#include +#include + +#include + +namespace caf +{ +template <> +void caf::AppEnum::setUp() +{ + addItem( RimCurveStyleRule::Mode::AUTO, "AUTO", "Auto" ); + addItem( RimCurveStyleRule::Mode::CUSTOM, "CUSTOM", "Custom" ); + setDefault( RimCurveStyleRule::Mode::AUTO ); +} +} // namespace caf + +CAF_PDM_SOURCE_INIT( RimCurveStyleRule, "RimCurveStyleRule" ); + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimCurveStyleRule::RimCurveStyleRule() +{ + CAF_PDM_InitObject( "Curve Style Rule" ); + + CAF_PDM_InitFieldNoDefault( &m_dimension, "Dimension", "Dimension" ); + CAF_PDM_InitFieldNoDefault( &m_itemName, "ItemName", "Item Name" ); + CAF_PDM_InitFieldNoDefault( &m_mode, "Mode", "Mode" ); + + CAF_PDM_InitFieldNoDefault( &m_curveAppearance, "CurveAppearance", "Curve Appearance" ); + m_curveAppearance = new RimPlotCurveAppearance; + m_curveAppearance.uiCapability()->setUiTreeChildrenHidden( true ); + connectAppearanceSignal(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimCurveStyleRule::connectAppearanceSignal() +{ + if ( m_curveAppearance() ) m_curveAppearance->appearanceChanged.connect( this, &RimCurveStyleRule::onAppearanceChanged ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimCurveStyleRule::initAfterRead() +{ + connectAppearanceSignal(); +} + +//-------------------------------------------------------------------------------------------------- +/// Editing any appearance value turns the item into a custom item +//-------------------------------------------------------------------------------------------------- +void RimCurveStyleRule::onAppearanceChanged( const caf::SignalEmitter* emitter ) +{ + m_mode = Mode::CUSTOM; + + if ( auto collection = firstAncestorOrThisOfType() ) collection->ruleChanged( this ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimCurveStyleRule::applyToCurve( RimPlotCurve* curve, bool applyColor, bool applySymbol ) const +{ + if ( m_mode() != Mode::CUSTOM ) return; + + auto curveAppearance = curve->curveAppearance(); + const auto color = curveAppearance->color(); + const auto symbol = curveAppearance->symbol(); + const auto label = curveAppearance->symbolLabel(); + const auto labelPosition = curveAppearance->symbolLabelPosition(); + + curveAppearance->copyFrom( *m_curveAppearance ); + + // Color and symbol are only controlled by the rule when its dimension is the one selected for them + if ( !applyColor ) curveAppearance->setColor( color ); + if ( !applySymbol ) curveAppearance->setSymbol( symbol ); + + // Labels are specific to each curve + curveAppearance->setSymbolLabel( label ); + curveAppearance->setSymbolLabelPosition( labelPosition ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::optional RimCurveStyleRule::overriddenColor() const +{ + if ( m_mode() != Mode::CUSTOM ) return std::nullopt; + return m_curveAppearance->color(); +} + +namespace +{ +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool matchesItem( RimCurveStyleRule::Dimension dimension, const QString& itemName, const RifEclipseSummaryAddress& address, const std::string& caseName ) +{ + return QString::fromStdString( RimCurveStyleSettings::dimensionValue( dimension, address, caseName ) ) == itemName; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::string ensembleCaseName( RimEnsembleCurveSet* curveSet ) +{ + return curveSet->summaryEnsemble() ? curveSet->summaryEnsemble()->name().toStdString() : std::string(); +} +} // namespace + +//-------------------------------------------------------------------------------------------------- +/// Shows what the global appearance gives for the item. The assigned color and symbol are used even if no +/// plot has a matching curve. +//-------------------------------------------------------------------------------------------------- +void RimCurveStyleRule::updateAppearanceFromCurves() +{ + copyAppearanceFromCurves(); + if ( m_autoColor ) m_curveAppearance->setColor( *m_autoColor ); + if ( m_autoSymbol ) m_curveAppearance->setSymbol( *m_autoSymbol ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimCurveStyleRule::setAutoAssigned( const std::optional& color, + const std::optional& symbol ) +{ + m_autoColor = color; + m_autoSymbol = symbol; +} + +//-------------------------------------------------------------------------------------------------- +/// Copies the appearance of the first matching curve +//-------------------------------------------------------------------------------------------------- +void RimCurveStyleRule::copyAppearanceFromCurves() +{ + auto project = RimProject::current(); + if ( !project ) return; + + for ( auto plot : project->descendantsIncludingThisOfType() ) + { + if ( !plot->useGlobalCurveAppearance() ) continue; + + for ( auto curve : plot->summaryCurves() ) + { + if ( matchesItem( m_dimension(), m_itemName(), curve->summaryAddressY(), RimCurveStyleSettings::caseName( curve->summaryCaseY() ) ) ) + { + m_curveAppearance->copyFrom( *curve->curveAppearance() ); + return; + } + } + + for ( auto curveSet : plot->ensembleCurveSetCollection()->curveSets() ) + { + if ( matchesItem( m_dimension(), m_itemName(), curveSet->summaryAddressY(), ensembleCaseName( curveSet ) ) ) + { + m_curveAppearance->setColor( curveSet->color() ); + return; + } + } + } +} +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimCurveStyleRule::Dimension RimCurveStyleRule::dimension() const +{ + return m_dimension(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RimCurveStyleRule::itemName() const +{ + return m_itemName(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimCurveStyleRule::setDimension( Dimension dimension ) +{ + m_dimension = dimension; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimCurveStyleRule::setItemName( const QString& itemName ) +{ + m_itemName = itemName; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimCurveStyleRule::setCustom( bool isCustom ) +{ + m_mode = isCustom ? Mode::CUSTOM : Mode::AUTO; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimCurveStyleRule::copySettingsFrom( const RimCurveStyleRule& other ) +{ + m_mode = other.m_mode(); + m_curveAppearance->copyFrom( *other.m_curveAppearance() ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +namespace +{ +std::optional> availableNamesCache; +} // namespace + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimCurveStyleRule::AvailableNamesCache::AvailableNamesCache() + : m_isOwner( !availableNamesCache ) +{ + if ( m_isOwner ) availableNamesCache.emplace(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimCurveStyleRule::AvailableNamesCache::~AvailableNamesCache() +{ + if ( m_isOwner ) availableNamesCache.reset(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QStringList RimCurveStyleRule::availableNames( Dimension dimension ) +{ + if ( availableNamesCache ) + { + auto it = availableNamesCache->find( dimension ); + if ( it != availableNamesCache->end() ) return it->second; + } + + if ( dimension == Dimension::NONE || !RimProject::current() ) return {}; + + std::set names; + if ( dimension == Dimension::WELL ) + { + for ( const auto& wellName : RiaSummaryTools::allSummaryWellNames() ) + { + names.insert( QString::fromStdString( wellName ) ); + } + } + else if ( dimension == Dimension::CASE ) + { + if ( auto project = RimProject::current() ) + { + for ( auto ensemble : project->summaryEnsembles() ) + { + names.insert( ensemble->name() ); + } + for ( auto summaryCase : project->allSummaryCases() ) + { + if ( !summaryCase->ensemble() ) names.insert( summaryCase->displayCaseName() ); + } + } + } + else if ( auto project = RimProject::current() ) + { + for ( auto summaryCase : project->allSummaryCases() ) + { + auto reader = summaryCase->summaryReader(); + if ( !reader ) continue; + + for ( const auto& address : reader->allResultAddresses() ) + { + const auto value = RimCurveStyleSettings::dimensionValue( dimension, address, {} ); + if ( !value.empty() ) names.insert( QString::fromStdString( value ) ); + } + } + } + + QStringList result; + for ( const auto& name : names ) + { + result.push_back( name ); + } + + if ( availableNamesCache ) ( *availableNamesCache )[dimension] = result; + + return result; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::map RimCurveStyleRule::appearancesFromPlots( Dimension dimension ) +{ + std::map appearances; + + auto project = RimProject::current(); + if ( !project ) return appearances; + + for ( auto plot : project->descendantsIncludingThisOfType() ) + { + if ( !plot->useGlobalCurveAppearance() ) continue; + + for ( auto curve : plot->summaryCurves() ) + { + const auto value = RimCurveStyleSettings::dimensionValue( dimension, + curve->summaryAddressY(), + RimCurveStyleSettings::caseName( curve->summaryCaseY() ) ); + + auto& appearance = appearances[QString::fromStdString( value )]; + if ( !appearance.color ) appearance.color = curve->color(); + if ( !appearance.symbol ) appearance.symbol = curve->symbol(); + } + + for ( auto curveSet : plot->ensembleCurveSetCollection()->curveSets() ) + { + const auto value = RimCurveStyleSettings::dimensionValue( dimension, curveSet->summaryAddressY(), ensembleCaseName( curveSet ) ); + + auto& appearance = appearances[QString::fromStdString( value )]; + if ( !appearance.color ) appearance.color = curveSet->color(); + } + } + + return appearances; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RimCurveStyleRule::isCustom() const +{ + return m_mode() == Mode::CUSTOM; +} +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimCurveStyleRule::fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue ) +{ + // The available names depend on the type + if ( changedField == &m_dimension ) m_itemName = QString(); + if ( changedField == &m_mode && m_mode() == Mode::AUTO ) updateAppearanceFromCurves(); + + if ( auto collection = firstAncestorOrThisOfType() ) collection->ruleChanged( this ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimCurveStyleRule::appendFieldsToUiOrdering( const QString& uiConfigName, caf::PdmUiOrdering& uiOrdering ) +{ + uiOrdering.add( &m_mode ); + + auto appearanceGroup = uiOrdering.addNewGroup( "Curve Style" ); + m_curveAppearance->uiOrdering( "AppearanceOrdering", *appearanceGroup ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimCurveStyleRule::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) +{ + uiOrdering.add( &m_dimension ); + uiOrdering.add( &m_itemName ); + uiOrdering.skipRemainingFields(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +caf::IconProvider RimCurveStyleRule::swatchIcon( const cvf::Color3f& color ) +{ + QPixmap pixmap( 16, 16 ); + pixmap.fill( Qt::transparent ); + + QPainter painter( &pixmap ); + painter.setPen( Qt::darkGray ); + painter.setBrush( RiaColorTools::toQColor( color ) ); + painter.drawRect( 1, 1, 13, 13 ); + + return caf::IconProvider( pixmap ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +caf::IconProvider RimCurveStyleRule::paletteIcon() +{ + QPixmap pixmap( 16, 16 ); + pixmap.fill( Qt::transparent ); + + QLinearGradient gradient( 0, 0, 16, 0 ); + gradient.setColorAt( 0.0, Qt::red ); + gradient.setColorAt( 0.5, Qt::green ); + gradient.setColorAt( 1.0, Qt::blue ); + + QPainter painter( &pixmap ); + painter.setPen( Qt::darkGray ); + painter.setBrush( gradient ); + painter.drawRect( 1, 1, 13, 13 ); + + return caf::IconProvider( pixmap ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +caf::IconProvider RimCurveStyleRule::symbolIcon() +{ + QPixmap pixmap( 16, 16 ); + pixmap.fill( Qt::transparent ); + + QPainter painter( &pixmap ); + painter.setRenderHint( QPainter::Antialiasing ); + painter.setPen( Qt::black ); + painter.setBrush( Qt::NoBrush ); + painter.drawEllipse( 2, 2, 5, 5 ); + painter.drawRect( 9, 2, 5, 5 ); + painter.drawLine( 2, 14, 8, 9 ); + painter.drawLine( 8, 9, 14, 14 ); + painter.drawLine( 2, 14, 14, 14 ); + + return caf::IconProvider( pixmap ); +} + +namespace +{ +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString dimensionIconPath( RimCurveStyleRule::Dimension dimension ) +{ + using D = RimCurveStyleRule::Dimension; + switch ( dimension ) + { + case D::CASE: + return ":/SummaryCase.svg"; + case D::VECTOR: + return ":/summary/components/images/misc.svg"; + case D::WELL: + return ":/summary/components/images/well.svg"; + case D::LGR: + return ":/LGR16x16.png"; + case D::GROUP: + return ":/summary/components/images/group.svg"; + case D::NETWORK: + return ":/summary/components/images/network.svg"; + case D::REGION: + return ":/summary/components/images/region.svg"; + case D::REGION_2_REGION: + return ":/summary/components/images/region-region.svg"; + case D::AQUIFER: + return ":/summary/components/images/aquifer.svg"; + case D::BLOCK: + case D::CONNECTION: + return ":/summary/components/images/block.svg"; + case D::SEGMENT: + return ":/summary/components/images/segment.svg"; + default: + return ":/DataVector.svg"; + } +} +} // namespace + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +caf::IconProvider RimCurveStyleRule::dimensionColorIcon( Dimension dimension, + const std::optional& color, + const std::optional& symbol ) +{ + QPixmap pixmap( 52, 16 ); + pixmap.fill( Qt::transparent ); + + QPainter painter( &pixmap ); + painter.drawPixmap( 0, 0, QIcon( dimensionIconPath( dimension ) ).pixmap( 16, 16 ) ); + + const QColor qColor = color ? RiaColorTools::toQColor( *color ) : QColor( Qt::black ); + + int x = 19; + if ( color ) + { + painter.setPen( Qt::darkGray ); + painter.setBrush( qColor ); + painter.drawRect( x, 1, 13, 13 ); + x += 17; + } + + if ( symbol && *symbol != RiuPlotCurveSymbol::SYMBOL_NONE ) + { + painter.drawImage( x, 1, RiuPlotCurveSymbolImageCreator::createSymbolImage( *symbol, QSize( 14, 14 ), qColor ) ); + } + + return caf::IconProvider( pixmap ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::optional RimCurveStyleRule::overriddenSymbol() const +{ + if ( m_mode() != Mode::CUSTOM ) return std::nullopt; + return m_curveAppearance->symbol(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +caf::IconProvider RimCurveStyleRule::dimensionIcon( Dimension dimension ) +{ + return caf::IconProvider( dimensionIconPath( dimension ) ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QList RimCurveStyleRule::calculateValueOptions( const caf::PdmFieldHandle* fieldNeedingOptions ) +{ + QList options; + + if ( fieldNeedingOptions == &m_mode ) + { + options.push_back( caf::PdmOptionItemInfo( "Auto", Mode::AUTO, false, caf::IconProvider( ":/Refresh.svg" ) ) ); + options.push_back( caf::PdmOptionItemInfo( "Custom", Mode::CUSTOM, false, caf::IconProvider( ":/edit.svg" ) ) ); + } + + return options; +} \ No newline at end of file diff --git a/ApplicationLibCode/ProjectDataModel/Summary/RimCurveStyleRule.h b/ApplicationLibCode/ProjectDataModel/Summary/RimCurveStyleRule.h new file mode 100644 index 00000000000..7593bf082a3 --- /dev/null +++ b/ApplicationLibCode/ProjectDataModel/Summary/RimCurveStyleRule.h @@ -0,0 +1,142 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// +#pragma once + +#include "RimCurveStyleSettings.h" +#include "RimPlotCurveAppearance.h" + +#include "RiuPlotCurveSymbol.h" + +#include "cafAppEnum.h" +#include "cafIconProvider.h" +#include "cafPdmField.h" +#include "cafPdmFieldCvfColor.h" +#include "cafPdmObject.h" + +#include "cvfColor3.h" + +#include +#include + +#include +#include + +class RimPlotCurve; + +//================================================================================================== +/// Project-stored curve appearance for one specific summary case, ensemble or well. The values are +/// edited in the curve appearance dock window. +//================================================================================================== +class RimCurveStyleRule : public caf::PdmObject +{ + CAF_PDM_HEADER_INIT; + +public: + using Dimension = RimCurveStyleSettings::Dimension; + + enum class Mode + { + AUTO, + CUSTOM + }; + + RimCurveStyleRule(); + + Dimension dimension() const; + QString itemName() const; + + void setDimension( Dimension dimension ); + void setItemName( const QString& itemName ); + void setCustom( bool isCustom ); + + // Copies the mode and appearance, not the item the rule applies to + void copySettingsFrom( const RimCurveStyleRule& other ); + + // Applies the rule to the curve if it is custom. Color and symbol are only applied when requested + void applyToCurve( RimPlotCurve* curve, bool applyColor, bool applySymbol ) const; + + static QStringList availableNames( Dimension dimension ); + + // Caches availableNames() while in scope, as finding them can require scanning all summary addresses + class AvailableNamesCache + { + public: + AvailableNamesCache(); + ~AvailableNamesCache(); + AvailableNamesCache( const AvailableNamesCache& ) = delete; + AvailableNamesCache& operator=( const AvailableNamesCache& ) = delete; + + private: + bool m_isOwner; + }; + + struct PlotAppearance + { + std::optional color; + std::optional symbol; + }; + + // The appearance of the first curve for each item in plots using the global appearance + static std::map appearancesFromPlots( Dimension dimension ); + + static caf::IconProvider swatchIcon( const cvf::Color3f& color ); + static caf::IconProvider paletteIcon(); + static caf::IconProvider symbolIcon(); + static caf::IconProvider dimensionIcon( Dimension dimension ); + + // Dimension icon followed by color and/or symbol swatches, when given. The symbol is drawn with the color if present. + static caf::IconProvider dimensionColorIcon( Dimension dimension, + const std::optional& color, + const std::optional& symbol = std::nullopt ); + bool isCustom() const; + + // The custom symbol, if the rule is custom + std::optional overriddenSymbol() const; + + // Adds the mode, color and symbol fields, to be edited for the object selected in the collection + void appendFieldsToUiOrdering( const QString& uiConfigName, caf::PdmUiOrdering& uiOrdering ); + + // The properties currently assigned automatically by the global appearance, shown while the item is Auto + void setAutoAssigned( const std::optional& color, const std::optional& symbol ); + void updateAppearanceFromCurves(); + + // The custom color, if the rule is custom + std::optional overriddenColor() const; + +protected: + void fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue ) override; + QList calculateValueOptions( const caf::PdmFieldHandle* fieldNeedingOptions ) override; + void defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) override; + void initAfterRead() override; + +private: + void copyAppearanceFromCurves(); + void connectAppearanceSignal(); + void onAppearanceChanged( const caf::SignalEmitter* emitter ); + +private: + caf::PdmField> m_dimension; + caf::PdmField m_itemName; + caf::PdmField> m_mode; + + // Color, symbol, line and fill settings that can be overridden + caf::PdmChildField m_curveAppearance; + + std::optional m_autoColor; + std::optional m_autoSymbol; +}; \ No newline at end of file diff --git a/ApplicationLibCode/ProjectDataModel/Summary/RimCurveStyleRules.cpp b/ApplicationLibCode/ProjectDataModel/Summary/RimCurveStyleRules.cpp new file mode 100644 index 00000000000..c088579535c --- /dev/null +++ b/ApplicationLibCode/ProjectDataModel/Summary/RimCurveStyleRules.cpp @@ -0,0 +1,659 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#include "RimCurveStyleRules.h" + +#include "RiaGuiApplication.h" + +#include "RimColorLegend.h" +#include "RimColorLegendCollection.h" +#include "RimColorLegendItem.h" +#include "RimCurveStyleRule.h" +#include "RimCurveStyleSettings.h" +#include "RimEnsembleCurveSet.h" +#include "RimEnsembleCurveSetCollection.h" +#include "RimProject.h" +#include "RimSummaryCurve.h" +#include "RimSummaryCurveAppearanceCalculator.h" +#include "RimSummaryPlot.h" + +#include "RiuPlotCurveSymbolImageCreator.h" +#include "RiuPlotMainWindow.h" + +#include "cafPdmUiLabel.h" +#include "cafPdmUiListEditor.h" + +#include +#include +#include + +CAF_PDM_SOURCE_INIT( RimCurveStyleRules, "RimCurveStyleRules" ); + +namespace caf +{ +template <> +void caf::AppEnum::setUp() +{ + using T = RimCurveStyleRules::AppearanceType; + addItem( T::COLOR, "COLOR", "Color" ); + addItem( T::SYMBOL, "SYMBOL", "Symbol" ); + setDefault( T::COLOR ); +} +} // namespace caf + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimCurveStyleRules::RimCurveStyleRules() +{ + CAF_PDM_InitObject( "Curve Style Rules" ); + + CAF_PDM_InitField( &m_useGlobalAppearance, + "UseGlobalAppearance", + false, + "Use Global Curve Appearance", + "", + "Assign curve appearance in all summary plots from the global curve appearance definition" ); + + CAF_PDM_InitFieldNoDefault( &m_settings, "Settings", "Settings" ); + m_settings.uiCapability()->setUiTreeChildrenHidden( true ); + m_settings.uiCapability()->setUiHidden( true ); + m_settings = new RimCurveStyleSettings; + + CAF_PDM_InitFieldNoDefault( &m_savedAppearances, "SavedAppearances", "Saved Appearances" ); + m_savedAppearances.uiCapability()->setUiTreeChildrenHidden( true ); + m_savedAppearances.uiCapability()->setUiHidden( true ); + + CAF_PDM_InitFieldNoDefault( &m_rules, "Rules", "Rules" ); + m_rules.uiCapability()->setUiTreeChildrenHidden( true ); + m_rules.uiCapability()->setUiHidden( true ); + + CAF_PDM_InitFieldNoDefault( &m_caseOrder, "CaseOrder", "Case Order" ); + m_caseOrder.uiCapability()->setUiHidden( true ); + CAF_PDM_InitFieldNoDefault( &m_wellOrder, "WellOrder", "Well Order" ); + m_wellOrder.uiCapability()->setUiHidden( true ); + + CAF_PDM_InitFieldNoDefault( &m_selectedItem, "SelectedItem", "Selected Item" ); + m_selectedItem.uiCapability()->setUiTreeChildrenHidden( true ); + m_selectedItem.uiCapability()->setUiHidden( true ); + m_selectedItem.xmlCapability()->disableIO(); + m_selectedItem = new RimCurveStyleRule; + + CAF_PDM_InitFieldNoDefault( &m_appearanceType, "AppearanceType", "Appearance Property" ); + m_appearanceType.xmlCapability()->disableIO(); + + CAF_PDM_InitFieldNoDefault( &m_autoColorLegend, "AutoColorLegend", "Color Palette" ); + + CAF_PDM_InitField( &m_symbolLegend, "SymbolLegend", 0, "Symbol Sequence" ); + m_symbolLegend.uiCapability()->setUiEditorTypeName( caf::PdmUiListEditor::uiEditorTypeName() ); + m_symbolLegend.uiCapability()->setUiLabelPosition( caf::PdmUiItemInfo::LabelPosition::TOP ); + m_symbolLegend.uiCapability()->setUiReadOnly( true ); + m_symbolLegend.uiCapability()->setAttribute( caf::PdmUiListEditor::Keys::HEIGHT_HINT, 120 ); + m_symbolLegend.xmlCapability()->disableIO(); + + CAF_PDM_InitFieldNoDefault( &m_currentType, "CurrentType", "Differentiate By" ); + m_currentType.registerGetMethod( this, &RimCurveStyleRules::currentDimension ); + m_currentType.registerSetMethod( this, &RimCurveStyleRules::setCurrentDimension ); + m_currentType.xmlCapability()->disableIO(); + + CAF_PDM_InitField( &m_currentItemIndex, "CurrentItemIndex", 0, "Items" ); + m_currentItemIndex.uiCapability()->setUiEditorTypeName( caf::PdmUiListEditor::uiEditorTypeName() ); + m_currentItemIndex.uiCapability()->setUiLabelPosition( caf::PdmUiItemInfo::LabelPosition::TOP ); + m_currentItemIndex.uiCapability()->setAttribute( caf::PdmUiListEditor::Keys::HEIGHT_HINT, 200 ); + m_currentItemIndex.uiCapability()->setAttribute( caf::PdmUiListEditor::Keys::ICON_SIZE, QSize( 52, 16 ) ); + m_currentItemIndex.xmlCapability()->disableIO(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimCurveStyleSettings* RimCurveStyleRules::settings() const +{ + return m_settings(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector RimCurveStyleRules::rules() const +{ + return m_rules.childrenByType(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimCurveStyleRule* RimCurveStyleRules::findOrCreateRule( RimCurveStyleRule::Dimension dimension, const QString& name ) +{ + if ( auto rule = findRule( dimension, name ) ) return rule; + + auto rule = new RimCurveStyleRule; + rule->setDimension( dimension ); + rule->setItemName( name ); + m_rules.push_back( rule ); + + return rule; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector RimCurveStyleRules::rulesFor( const RifEclipseSummaryAddress& address, const std::string& caseName ) const +{ + const auto colorDimension = m_settings->colorDimension(); + const auto symbolDimension = m_settings->symbolDimension(); + + std::vector result; + for ( auto rule : m_rules ) + { + if ( !rule->isCustom() ) continue; + + const auto dimension = rule->dimension(); + if ( dimension != colorDimension && dimension != symbolDimension ) continue; + + const auto value = RimCurveStyleSettings::dimensionValue( dimension, address, caseName ); + if ( !value.empty() && QString::fromStdString( value ) == rule->itemName() ) result.push_back( rule ); + } + + // Fixed precedence by dimension, so the result does not depend on the order the rules were created in + std::stable_sort( result.begin(), + result.end(), + []( const RimCurveStyleRule* a, const RimCurveStyleRule* b ) { return a->dimension() < b->dimension(); } ); + + return result; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimCurveStyleRules::rulesChanged() +{ + auto project = RimProject::current(); + if ( !project ) return; + + RimCurveStyleRule::AvailableNamesCache namesCache; + for ( auto plot : project->descendantsIncludingThisOfType() ) + { + plot->updateCurveAppearanceFromGlobalSettings(); + } +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimCurveStyleRules::ruleChanged( RimCurveStyleRule* rule ) +{ + if ( rule == m_selectedItem() ) + { + // Rules are only stored for items that have been made custom + auto stored = rule->isCustom() ? findOrCreateRule( rule->dimension(), rule->itemName() ) + : findRule( rule->dimension(), rule->itemName() ); + if ( stored ) stored->copySettingsFrom( *rule ); + } + + rulesChanged(); + updateConnectedEditors(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RimCurveStyleRules::controlsColor( const RifEclipseSummaryAddress& address, const std::string& caseName ) const +{ + const auto dimension = m_settings->colorDimension(); + if ( !m_useGlobalAppearance() || dimension == RimCurveStyleRule::Dimension::NONE ) return false; + + return !RimCurveStyleSettings::dimensionValue( dimension, address, caseName ).empty(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RimCurveStyleRules::controlsSymbol( const RifEclipseSummaryAddress& address, const std::string& caseName ) const +{ + const auto dimension = m_settings->symbolDimension(); + if ( !m_useGlobalAppearance() || dimension == RimCurveStyleRule::Dimension::NONE ) return false; + + return !RimCurveStyleSettings::dimensionValue( dimension, address, caseName ).empty(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimCurveStyleRules::registerItemNames( RimCurveStyleRule::Dimension dimension, const QStringList& names ) +{ + auto order = itemOrder( dimension ); + if ( !order ) return; + + auto values = ( *order )(); + const std::set registered( values.begin(), values.end() ); + for ( const auto& name : names ) + { + if ( !registered.contains( name ) ) values.push_back( name ); + } + + if ( values.size() != ( *order )().size() ) order->setValue( values ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::map RimCurveStyleRules::itemIndices( RimCurveStyleRule::Dimension dimension ) const +{ + auto order = itemOrder( dimension ); + if ( !order ) return {}; + + std::map indices; + + int index = 0; + for ( const auto& name : ( *order )() ) + { + if ( indices.emplace( name, index ).second ) index++; + } + + for ( const auto& name : RimCurveStyleRule::availableNames( dimension ) ) + { + if ( indices.emplace( name, index ).second ) index++; + } + + return indices; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +caf::PdmField>* RimCurveStyleRules::itemOrder( RimCurveStyleRule::Dimension dimension ) +{ + if ( dimension == RimCurveStyleRule::Dimension::CASE ) return &m_caseOrder; + if ( dimension == RimCurveStyleRule::Dimension::WELL ) return &m_wellOrder; + return nullptr; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +const caf::PdmField>* RimCurveStyleRules::itemOrder( RimCurveStyleRule::Dimension dimension ) const +{ + return const_cast( this )->itemOrder( dimension ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimCurveStyleRules::addGlobalAppearanceLink( caf::PdmUiOrdering& uiOrdering, const QString& text ) +{ + auto label = uiOrdering.addNewLabel( text ); + label->setLinkActivatedCallback( + []( const QString& ) + { + if ( auto window = RiaGuiApplication::instance()->mainPlotWindow() ) window->showCurveAppearance(); + } ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RimCurveStyleRules::useGlobalAppearance() const +{ + return m_useGlobalAppearance(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimCurveStyleRules::setUseGlobalAppearance( bool enable ) +{ + if ( m_useGlobalAppearance() == enable ) return; + + m_useGlobalAppearance = enable; + applyGlobalAppearanceMode(); +} + +//-------------------------------------------------------------------------------------------------- +/// Applies the global appearance to all plots when enabled, and restores the original appearance when disabled +//-------------------------------------------------------------------------------------------------- +void RimCurveStyleRules::applyGlobalAppearanceMode() +{ + auto project = RimProject::current(); + if ( !project ) return; + + if ( m_useGlobalAppearance() ) + { + RimCurveStyleRule::AvailableNamesCache namesCache; + + m_savedAppearances.deleteChildren(); + for ( auto plot : project->descendantsIncludingThisOfType() ) + { + saveAppearance( plot ); + plot->updateCurveAppearanceFromGlobalSettings(); + } + } + else + { + restoreAppearances(); + m_savedAppearances.deleteChildren(); + } + + updateConnectedEditors(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimCurveStyleRules::saveAppearance( RimSummaryPlot* plot ) +{ + for ( auto curve : plot->summaryCurves() ) + { + auto saved = new RimSavedCurveAppearance; + saved->setCurve( curve ); + m_savedAppearances.push_back( saved ); + } + + for ( auto curveSet : plot->ensembleCurveSetCollection()->curveSets() ) + { + auto saved = new RimSavedCurveAppearance; + saved->setEnsembleCurveSet( curveSet ); + m_savedAppearances.push_back( saved ); + } +} + +//-------------------------------------------------------------------------------------------------- +/// Curves without a saved appearance, typically created while the global appearance was enabled, get the +/// calculated default appearance +//-------------------------------------------------------------------------------------------------- +void RimCurveStyleRules::restoreAppearances() +{ + auto project = RimProject::current(); + if ( !project ) return; + + std::set restored; + for ( auto saved : m_savedAppearances ) + { + if ( auto object = saved->restore() ) restored.insert( object ); + } + + for ( auto plot : project->descendantsIncludingThisOfType() ) + { + std::vector curves; + for ( auto curve : plot->summaryCurves() ) + { + if ( !restored.contains( curve ) ) curves.push_back( curve ); + } + if ( !curves.empty() ) plot->applyDefaultCurveAppearances( curves ); + + std::vector curveSets; + for ( auto curveSet : plot->ensembleCurveSetCollection()->curveSets() ) + { + if ( !restored.contains( curveSet ) ) curveSets.push_back( curveSet ); + } + if ( !curveSets.empty() ) plot->applyDefaultCurveAppearances( curveSets ); + + plot->updateConnectedEditors(); + plot->loadDataAndUpdate(); + } +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::optional RimCurveStyleRules::autoLegendColor( int index ) const +{ + if ( !m_autoColorLegend() || index < 0 ) return std::nullopt; + + const auto items = m_autoColorLegend()->colorLegendItems(); + if ( items.empty() ) return std::nullopt; + + return items[index % items.size()]->color(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QList RimCurveStyleRules::calculateValueOptions( const caf::PdmFieldHandle* fieldNeedingOptions ) +{ + QList options; + + if ( fieldNeedingOptions == &m_autoColorLegend ) + { + options.push_back( caf::PdmOptionItemInfo( "Default Palette", nullptr ) ); + if ( auto project = RimProject::current() ) + { + for ( auto legend : project->colorLegendCollection()->allColorLegends() ) + { + options.push_back( caf::PdmOptionItemInfo( legend->colorLegendName(), legend, false, legend->paletteIconProvider() ) ); + } + } + } + else if ( fieldNeedingOptions == &m_symbolLegend ) + { + const int count = static_cast( caf::AppEnum::size() ) - 1; + for ( int i = 0; i < count; i++ ) + { + const auto symbol = RimSummaryCurveAppearanceCalculator::cycledSymbol( i ); + const auto image = RiuPlotCurveSymbolImageCreator::createSymbolImage( symbol, QSize( 14, 14 ), QColor( Qt::black ) ); + options.push_back( caf::PdmOptionItemInfo( caf::AppEnum::uiText( symbol ), + i, + false, + caf::IconProvider( QPixmap::fromImage( image ) ) ) ); + } + } + else if ( fieldNeedingOptions == &m_currentType ) + { + using D = RimCurveStyleRule::Dimension; + for ( size_t i = 0; i < caf::AppEnum::size(); i++ ) + { + const auto dimension = caf::AppEnum::fromIndex( i ); + options.push_back( caf::PdmOptionItemInfo( caf::AppEnum::uiText( dimension ), static_cast( dimension ) ) ); + } + } + else if ( fieldNeedingOptions == &m_currentItemIndex ) + { + RimCurveStyleRule::AvailableNamesCache namesCache; + + const bool isColorDim = m_appearanceType() == AppearanceType::COLOR; + const bool isSymbolDim = m_appearanceType() == AppearanceType::SYMBOL; + const auto plotAppearances = RimCurveStyleRule::appearancesFromPlots( m_currentType() ); + const auto indices = itemIndices( m_currentType() ); + for ( const auto& name : RimCurveStyleRule::availableNames( m_currentType() ) ) + { + const auto customRule = findRule( m_currentType(), name ); + const bool isCustom = customRule && customRule->isCustom(); + const auto indexIt = indices.find( name ); + const int index = indexIt != indices.end() ? indexIt->second : static_cast( options.size() ); + + std::optional autoColor; + std::optional autoSymbol; + assignedAppearance( name, index, plotAppearances, autoColor, autoSymbol ); + + std::optional color; + if ( isColorDim && autoColor ) color = isCustom ? customRule->overriddenColor().value_or( *autoColor ) : autoColor; + + std::optional symbol; + if ( isSymbolDim && autoSymbol ) symbol = isCustom ? customRule->overriddenSymbol().value_or( *autoSymbol ) : autoSymbol; + const auto icon = RimCurveStyleRule::dimensionColorIcon( m_currentType(), color, symbol ); + options.push_back( + caf::PdmOptionItemInfo( isCustom ? name + " [Custom]" : name + " [Auto]", static_cast( options.size() ), false, icon ) ); + } + } + + return options; +} + +//-------------------------------------------------------------------------------------------------- +/// The color and symbol the global appearance assigns to the item at the given position +//-------------------------------------------------------------------------------------------------- +void RimCurveStyleRules::assignedAppearance( const QString& name, + int index, + const std::map& plotAppearances, + std::optional& color, + std::optional& symbol ) const +{ + const bool projectWide = m_currentType() == RimCurveStyleRule::Dimension::CASE || m_currentType() == RimCurveStyleRule::Dimension::WELL; + + const auto fromPlots = plotAppearances.find( name ); + const bool inPlots = fromPlots != plotAppearances.end(); + + if ( projectWide ) color = RimSummaryCurveAppearanceCalculator::cycledPaletteColor( index ); + if ( !color && inPlots ) color = fromPlots->second.color; + if ( !color ) color = RimSummaryCurveAppearanceCalculator::cycledPaletteColor( index ); + + if ( projectWide ) symbol = RimSummaryCurveAppearanceCalculator::cycledSymbol( index ); + if ( !symbol && inPlots ) symbol = fromPlots->second.symbol; + if ( !symbol ) symbol = RimSummaryCurveAppearanceCalculator::cycledSymbol( index ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimCurveStyleRule* RimCurveStyleRules::findRule( RimCurveStyleRule::Dimension dimension, const QString& name ) const +{ + for ( auto item : m_rules ) + { + if ( item->dimension() == dimension && item->itemName() == name ) return item; + } + + return nullptr; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimCurveStyleRules::fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue ) +{ + RimCurveStyleRule::AvailableNamesCache namesCache; + + if ( changedField == &m_useGlobalAppearance ) + { + applyGlobalAppearanceMode(); + return; + } + + if ( changedField == &m_autoColorLegend ) + { + rulesChanged(); + } + else if ( changedField == &m_appearanceType ) + { + m_currentItemIndex = 0; + } + else if ( changedField == &m_currentType ) + { + m_currentItemIndex = 0; + rulesChanged(); + } + + updateConnectedEditors(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RimCurveStyleRules::currentItemName() const +{ + const auto names = RimCurveStyleRule::availableNames( m_currentType() ); + const int index = m_currentItemIndex(); + return index >= 0 && index < names.size() ? names[index] : QString(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +caf::AppEnum RimCurveStyleRules::currentDimension() const +{ + auto settings = m_settings(); + if ( !settings ) return RimCurveStyleRule::Dimension::NONE; + + return m_appearanceType() == AppearanceType::COLOR ? settings->colorDimension() : settings->symbolDimension(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimCurveStyleRules::setCurrentDimension( const caf::AppEnum& dimension ) +{ + auto settings = m_settings(); + if ( !settings ) return; + + if ( m_appearanceType() == AppearanceType::COLOR ) + settings->setColorDimension( dimension ); + else + settings->setSymbolDimension( dimension ); +} + +//-------------------------------------------------------------------------------------------------- +/// Shows the stored custom settings of the selected item, or the appearance assigned automatically. Only the +/// selected item object is changed, it is not stored in the project. +//-------------------------------------------------------------------------------------------------- +bool RimCurveStyleRules::updateSelectedItem() +{ + const QString itemName = currentItemName(); + if ( itemName.isEmpty() ) return false; + + auto selected = m_selectedItem(); + selected->setDimension( m_currentType() ); + selected->setItemName( itemName ); + + auto stored = findRule( m_currentType(), itemName ); + if ( stored && stored->isCustom() ) + { + selected->copySettingsFrom( *stored ); + return true; + } + + const auto indices = itemIndices( m_currentType() ); + const auto indexIt = indices.find( itemName ); + const int index = indexIt != indices.end() ? indexIt->second : m_currentItemIndex(); + + std::optional autoColor; + std::optional autoSymbol; + assignedAppearance( itemName, index, RimCurveStyleRule::appearancesFromPlots( m_currentType() ), autoColor, autoSymbol ); + + selected->setCustom( false ); + selected->setAutoAssigned( m_appearanceType() == AppearanceType::COLOR ? autoColor : std::nullopt, + m_appearanceType() == AppearanceType::SYMBOL ? autoSymbol : std::nullopt ); + selected->updateAppearanceFromCurves(); + + return true; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimCurveStyleRules::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) +{ + RimCurveStyleRule::AvailableNamesCache namesCache; + + uiOrdering.add( &m_useGlobalAppearance ); + uiOrdering.add( &m_appearanceType ); + uiOrdering.add( &m_currentType ); + + auto rulesGroup = uiOrdering.addNewGroup( "Assigned Appearance" ); + if ( m_appearanceType() == AppearanceType::COLOR ) + rulesGroup->add( &m_autoColorLegend ); + else + rulesGroup->add( &m_symbolLegend ); + rulesGroup->add( &m_currentItemIndex ); + + if ( updateSelectedItem() ) + { + auto appearanceGroup = uiOrdering.addNewGroup( "Selected Item" ); + m_selectedItem->appendFieldsToUiOrdering( uiConfigName, *appearanceGroup ); + } + + uiOrdering.skipRemainingFields(); +} diff --git a/ApplicationLibCode/ProjectDataModel/Summary/RimCurveStyleRules.h b/ApplicationLibCode/ProjectDataModel/Summary/RimCurveStyleRules.h new file mode 100644 index 00000000000..74d93b839c2 --- /dev/null +++ b/ApplicationLibCode/ProjectDataModel/Summary/RimCurveStyleRules.h @@ -0,0 +1,137 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// +#pragma once + +#include "RimCurveStyleRule.h" +#include "RimPlotCurveAppearance.h" +#include "RimSavedCurveAppearance.h" + +#include "cafPdmChildArrayField.h" +#include "cafPdmChildField.h" +#include "cafPdmField.h" +#include "cafPdmObject.h" +#include "cafPdmProxyValueField.h" +#include "cafPdmPtrField.h" + +#include "cvfColor3.h" + +#include +#include +#include +#include + +class RimColorLegend; +class RimEnsembleCurveSet; +class RimPlotCurve; +class RimSummaryPlot; +class RifEclipseSummaryAddress; + +//================================================================================================== +/// Project-stored list of per-item style rules for the global curve appearance. +//================================================================================================== +class RimCurveStyleRules : public caf::PdmObject +{ + CAF_PDM_HEADER_INIT; + +public: + RimCurveStyleRules(); + + std::vector rules() const; + RimCurveStyleRule* findOrCreateRule( RimCurveStyleRule::Dimension dimension, const QString& name ); + + // Custom rules for the dimensions selected for color or symbol, ordered so that later entries win: by dimension + std::vector rulesFor( const RifEclipseSummaryAddress& address, const std::string& caseName ) const; + + RimCurveStyleSettings* settings() const; + + // Re-applies the global appearance to all plots using it + void rulesChanged(); + + // Stores custom settings edited for the selected item, and re-applies the global appearance + void ruleChanged( RimCurveStyleRule* rule ); + + // Whether the global appearance assigns the color or symbol of a curve with the given address and case + bool controlsColor( const RifEclipseSummaryAddress& address, const std::string& caseName ) const; + bool controlsSymbol( const RifEclipseSummaryAddress& address, const std::string& caseName ) const; + + // Index used to pick the palette color and symbol for a case or well name. Indices are kept in the project so that + // existing items keep their appearance when new cases or wells are added. + // Names not yet registered get the indices they would get if registered now. + void registerItemNames( RimCurveStyleRule::Dimension dimension, const QStringList& names ); + std::map itemIndices( RimCurveStyleRule::Dimension dimension ) const; + + // Color from the project color legend used for auto colors, if one is selected + std::optional autoLegendColor( int index ) const; + + // Configures a field as a label with a link that shows the global curve appearance + static void addGlobalAppearanceLink( caf::PdmUiOrdering& uiOrdering, const QString& text ); + + // The global appearance applies to all summary plots in the project + bool useGlobalAppearance() const; + void setUseGlobalAppearance( bool enable ); + +protected: + QList calculateValueOptions( const caf::PdmFieldHandle* fieldNeedingOptions ) override; + void fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue ) override; + void defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) override; + +public: + enum class AppearanceType + { + COLOR, + SYMBOL + }; + +private: + void assignedAppearance( const QString& name, + int index, + const std::map& plotAppearances, + std::optional& color, + std::optional& symbol ) const; + RimCurveStyleRule* findRule( RimCurveStyleRule::Dimension dimension, const QString& name ) const; + QString currentItemName() const; + bool updateSelectedItem(); + + // The dimension controlling the selected appearance type, as stored in the global settings + caf::AppEnum currentDimension() const; + void setCurrentDimension( const caf::AppEnum& dimension ); + + caf::PdmField>* itemOrder( RimCurveStyleRule::Dimension dimension ); + const caf::PdmField>* itemOrder( RimCurveStyleRule::Dimension dimension ) const; + + void applyGlobalAppearanceMode(); + void saveAppearance( RimSummaryPlot* plot ); + void restoreAppearances(); + + // The curve appearance before the global appearance was enabled, restored when disabled + caf::PdmChildArrayField m_savedAppearances; + caf::PdmField m_useGlobalAppearance; + + caf::PdmChildField m_settings; + caf::PdmChildArrayField m_rules; + caf::PdmField> m_caseOrder; + caf::PdmField> m_wellOrder; + caf::PdmField> m_appearanceType; + caf::PdmProxyValueField> m_currentType; + caf::PdmPtrField m_autoColorLegend; + caf::PdmField m_symbolLegend; + caf::PdmField m_currentItemIndex; + + // Edited in the dock window for the selected item. Not stored, custom settings are copied to a rule in m_rules. + caf::PdmChildField m_selectedItem; +}; diff --git a/ApplicationLibCode/ProjectDataModel/Summary/RimCurveStyleSettings.cpp b/ApplicationLibCode/ProjectDataModel/Summary/RimCurveStyleSettings.cpp new file mode 100644 index 00000000000..17d43361959 --- /dev/null +++ b/ApplicationLibCode/ProjectDataModel/Summary/RimCurveStyleSettings.cpp @@ -0,0 +1,197 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#include "RimCurveStyleSettings.h" + +#include "RifEclipseSummaryAddress.h" + +#include "RimCurveStyleRules.h" +#include "RimSummaryCase.h" +#include "RimSummaryEnsemble.h" + +namespace caf +{ +template <> +void caf::AppEnum::setUp() +{ + using D = RimCurveStyleSettings::Dimension; + addItem( D::NONE, "NONE", "None" ); + addItem( D::CASE, "CASE", "Summary Case" ); + addItem( D::VECTOR, "VECTOR", "Vector" ); + addItem( D::WELL, "WELL", "Well" ); + addItem( D::GROUP, "GROUP", "Group" ); + addItem( D::NETWORK, "NETWORK", "Network" ); + addItem( D::REGION, "REGION", "Region" ); + addItem( D::REGION_2_REGION, "REGION_2_REGION", "Region-Region" ); + addItem( D::AQUIFER, "AQUIFER", "Aquifer" ); + addItem( D::LGR, "LGR", "LGR" ); + addItem( D::BLOCK, "BLOCK", "Block" ); + addItem( D::CONNECTION, "CONNECTION", "Connection" ); + addItem( D::SEGMENT, "SEGMENT", "Segment" ); + setDefault( D::NONE ); +} +} // namespace caf + +CAF_PDM_SOURCE_INIT( RimCurveStyleSettings, "RimCurveStyleSettings" ); + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimCurveStyleSettings::RimCurveStyleSettings() +{ + CAF_PDM_InitObject( "Global Curve Appearance" ); + + CAF_PDM_InitField( &m_colorDimension, "ColorDimension", DimensionEnum( Dimension::CASE ), "Color", "", "Data dimension controlling the curve color" ); + CAF_PDM_InitField( &m_symbolDimension, + "SymbolDimension", + DimensionEnum( Dimension::NONE ), + "Symbol", + "", + "Data dimension controlling the curve symbol" ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimCurveStyleSettings::Dimension RimCurveStyleSettings::colorDimension() const +{ + return m_colorDimension(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimCurveStyleSettings::Dimension RimCurveStyleSettings::symbolDimension() const +{ + return m_symbolDimension(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimCurveStyleSettings::setColorDimension( Dimension dimension ) +{ + m_colorDimension = dimension; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimCurveStyleSettings::setSymbolDimension( Dimension dimension ) +{ + m_symbolDimension = dimension; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::string RimCurveStyleSettings::caseName( const RimSummaryCase* summaryCase ) +{ + if ( !summaryCase ) return {}; + if ( summaryCase->ensemble() ) return summaryCase->ensemble()->name().toStdString(); + return summaryCase->displayCaseName().toStdString(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::string RimCurveStyleSettings::dimensionValue( Dimension dimension, const RifEclipseSummaryAddress& address, const std::string& caseName ) +{ + using Category = RifEclipseSummaryAddressDefines::SummaryCategory; + + switch ( dimension ) + { + case Dimension::CASE: + return caseName; + + case Dimension::VECTOR: + { + std::string name = address.vectorName(); + if ( address.isHistoryVector() && !name.empty() ) name.pop_back(); + return name; + } + + case Dimension::WELL: + return address.wellName(); + + case Dimension::GROUP: + return address.groupName(); + + case Dimension::NETWORK: + return address.networkName(); + + case Dimension::REGION: + return address.category() == Category::SUMMARY_REGION ? address.itemUiText() : std::string(); + + case Dimension::REGION_2_REGION: + return address.category() == Category::SUMMARY_REGION_2_REGION ? address.itemUiText() : std::string(); + + case Dimension::AQUIFER: + return address.category() == Category::SUMMARY_AQUIFER ? address.itemUiText() : std::string(); + + case Dimension::LGR: + return address.lgrName(); + + case Dimension::BLOCK: + return address.category() == Category::SUMMARY_BLOCK || address.category() == Category::SUMMARY_BLOCK_LGR ? address.itemUiText() + : std::string(); + + case Dimension::CONNECTION: + return address.category() == Category::SUMMARY_WELL_COMPLETION || address.category() == Category::SUMMARY_WELL_CONNECTION || + address.category() == Category::SUMMARY_WELL_CONNECTION_LGR + ? address.itemUiText() + : std::string(); + + case Dimension::SEGMENT: + return address.category() == Category::SUMMARY_WELL_SEGMENT ? address.itemUiText() : std::string(); + + default: + return std::string(); + } +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimCurveStyleSettings::fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue ) +{ + // The collection list shows colors that depend on the global appearance + if ( auto collection = firstAncestorOrThisOfType() ) + { + collection->rulesChanged(); + collection->updateConnectedEditors(); + } +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimCurveStyleSettings::appendFieldsToUiOrdering( caf::PdmUiOrdering& uiOrdering ) +{ + uiOrdering.add( &m_colorDimension ); + uiOrdering.add( &m_symbolDimension ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimCurveStyleSettings::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) +{ + appendFieldsToUiOrdering( uiOrdering ); + uiOrdering.skipRemainingFields(); +} diff --git a/ApplicationLibCode/ProjectDataModel/Summary/RimCurveStyleSettings.h b/ApplicationLibCode/ProjectDataModel/Summary/RimCurveStyleSettings.h new file mode 100644 index 00000000000..b8dc2967afe --- /dev/null +++ b/ApplicationLibCode/ProjectDataModel/Summary/RimCurveStyleSettings.h @@ -0,0 +1,79 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// +#pragma once + +#include "cafAppEnum.h" +#include "cafPdmField.h" +#include "cafPdmObject.h" + +#include + +class RifEclipseSummaryAddress; +class RimSummaryCase; + +//================================================================================================== +/// Global definition of which data dimension controls the curve color and the curve symbol. +/// Used by summary plots that have "Use Global Curve Appearance" enabled. +//================================================================================================== +class RimCurveStyleSettings : public caf::PdmObject +{ + CAF_PDM_HEADER_INIT; + +public: + enum class Dimension + { + NONE, + CASE, + VECTOR, + WELL, + GROUP, + NETWORK, + REGION, + REGION_2_REGION, + AQUIFER, + LGR, + BLOCK, + CONNECTION, + SEGMENT + }; + using DimensionEnum = caf::AppEnum; + + RimCurveStyleSettings(); + + Dimension colorDimension() const; + Dimension symbolDimension() const; + void setColorDimension( Dimension dimension ); + void setSymbolDimension( Dimension dimension ); + + // The value of the dimension for the address, empty if the address has no value for the dimension + static std::string dimensionValue( Dimension dimension, const RifEclipseSummaryAddress& address, const std::string& caseName ); + + // The case name used by dimensionValue: ensemble name for ensemble members, otherwise the display name + static std::string caseName( const RimSummaryCase* summaryCase ); + + // Adds the color and symbol fields, used both here and when shown together with other items + void appendFieldsToUiOrdering( caf::PdmUiOrdering& uiOrdering ); + +protected: + void defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) override; + void fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue ) override; + +private: + caf::PdmField m_colorDimension; + caf::PdmField m_symbolDimension; +}; diff --git a/ApplicationLibCode/ProjectDataModel/Summary/RimSavedCurveAppearance.cpp b/ApplicationLibCode/ProjectDataModel/Summary/RimSavedCurveAppearance.cpp new file mode 100644 index 00000000000..c488e150ff5 --- /dev/null +++ b/ApplicationLibCode/ProjectDataModel/Summary/RimSavedCurveAppearance.cpp @@ -0,0 +1,76 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#include "RimSavedCurveAppearance.h" + +#include "RimEnsembleCurveSet.h" +#include "RimPlotCurve.h" +#include "RimPlotCurveAppearance.h" + +CAF_PDM_SOURCE_INIT( RimSavedCurveAppearance, "RimSavedCurveAppearance" ); + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimSavedCurveAppearance::RimSavedCurveAppearance() +{ + CAF_PDM_InitObject( "Saved Curve Appearance" ); + + CAF_PDM_InitFieldNoDefault( &m_curve, "Curve", "Curve" ); + CAF_PDM_InitFieldNoDefault( &m_appearance, "CurveAppearance", "Curve Appearance" ); + CAF_PDM_InitFieldNoDefault( &m_ensembleCurveSet, "EnsembleCurveSet", "Ensemble Curve Set" ); + CAF_PDM_InitFieldNoDefault( &m_ensembleColor, "EnsembleColor", "Ensemble Color" ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimSavedCurveAppearance::setCurve( RimPlotCurve* curve ) +{ + m_curve = curve; + m_appearance = curve->curveAppearance()->copyObject(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimSavedCurveAppearance::setEnsembleCurveSet( RimEnsembleCurveSet* curveSet ) +{ + m_ensembleCurveSet = curveSet; + m_ensembleColor = curveSet->color(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +caf::PdmObject* RimSavedCurveAppearance::restore() const +{ + if ( m_curve() && m_appearance() ) + { + m_curve()->curveAppearance()->copyFrom( *m_appearance() ); + return m_curve(); + } + + if ( m_ensembleCurveSet() ) + { + m_ensembleCurveSet()->setColorAndUpdateCurves( m_ensembleColor() ); + return m_ensembleCurveSet(); + } + + return nullptr; +} diff --git a/ApplicationLibCode/ProjectDataModel/Summary/RimSavedCurveAppearance.h b/ApplicationLibCode/ProjectDataModel/Summary/RimSavedCurveAppearance.h new file mode 100644 index 00000000000..58f906bde7b --- /dev/null +++ b/ApplicationLibCode/ProjectDataModel/Summary/RimSavedCurveAppearance.h @@ -0,0 +1,53 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// +#pragma once + +#include "cafPdmChildField.h" +#include "cafPdmField.h" +#include "cafPdmFieldCvfColor.h" +#include "cafPdmObject.h" +#include "cafPdmPtrField.h" + +class RimEnsembleCurveSet; +class RimPlotCurve; +class RimPlotCurveAppearance; + +//================================================================================================== +/// The appearance of a curve or ensemble curve set before the global curve appearance was enabled. Stored in the +/// project so it can be restored after a reload, and referenced by PDM pointers so deleted objects are not matched. +//================================================================================================== +class RimSavedCurveAppearance : public caf::PdmObject +{ + CAF_PDM_HEADER_INIT; + +public: + RimSavedCurveAppearance(); + + void setCurve( RimPlotCurve* curve ); + void setEnsembleCurveSet( RimEnsembleCurveSet* curveSet ); + + // Restores the saved appearance and returns the curve or curve set, nullptr if it no longer exists + caf::PdmObject* restore() const; + +private: + caf::PdmPtrField m_curve; + caf::PdmChildField m_appearance; + + caf::PdmPtrField m_ensembleCurveSet; + caf::PdmField m_ensembleColor; +}; diff --git a/ApplicationLibCode/UnitTests/CMakeLists.txt b/ApplicationLibCode/UnitTests/CMakeLists.txt index 865b82ae993..d244ed2fd8f 100644 --- a/ApplicationLibCode/UnitTests/CMakeLists.txt +++ b/ApplicationLibCode/UnitTests/CMakeLists.txt @@ -147,6 +147,8 @@ set(SOURCE_UNITTEST_FILES ${CMAKE_CURRENT_LIST_DIR}/opm-summary-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RifEclipseTextFileReader-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RiaSummaryStringTools-Test.cpp + ${CMAKE_CURRENT_LIST_DIR}/RimCurveStyleSettings-Test.cpp + ${CMAKE_CURRENT_LIST_DIR}/RimCurveStyleRules-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RiaVariableMapper-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RifPressureDepthTextFileReader-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RifRevealCsvSectionSummaryReader-Test.cpp diff --git a/ApplicationLibCode/UnitTests/RimCurveStyleRules-Test.cpp b/ApplicationLibCode/UnitTests/RimCurveStyleRules-Test.cpp new file mode 100644 index 00000000000..4cb34d7285e --- /dev/null +++ b/ApplicationLibCode/UnitTests/RimCurveStyleRules-Test.cpp @@ -0,0 +1,124 @@ +#include "gtest/gtest.h" + +#include "RifEclipseSummaryAddress.h" + +#include "RimCurveStyleRule.h" +#include "RimCurveStyleRules.h" +#include "RimCurveStyleSettings.h" +#include "RimPlotCurveAppearance.h" +#include "RimSavedCurveAppearance.h" +#include "RimSummaryCurve.h" + +using D = RimCurveStyleSettings::Dimension; + +//-------------------------------------------------------------------------------------------------- +/// Rules for the selected dimensions are returned ordered by dimension, independent of creation order +//-------------------------------------------------------------------------------------------------- +TEST( RimCurveStyleRulesTest, RulesForOrderedByDimension ) +{ + RimCurveStyleRules rules; + rules.settings()->setColorDimension( D::WELL ); + rules.settings()->setSymbolDimension( D::CASE ); + + auto wellRule = rules.findOrCreateRule( D::WELL, "A-1" ); + wellRule->setCustom( true ); + rules.findOrCreateRule( D::WELL, "B-2" )->setCustom( true ); + rules.findOrCreateRule( D::VECTOR, "WOPR" )->setCustom( true ); + rules.findOrCreateRule( D::CASE, "auto case" ); + auto caseRule = rules.findOrCreateRule( D::CASE, "case" ); + caseRule->setCustom( true ); + + EXPECT_EQ( wellRule, rules.findOrCreateRule( D::WELL, "A-1" ) ); + + const auto matching = rules.rulesFor( RifEclipseSummaryAddress::wellAddress( "WOPR", "A-1" ), "case" ); + + ASSERT_EQ( 2u, matching.size() ); + EXPECT_EQ( caseRule, matching[0] ); + EXPECT_EQ( wellRule, matching[1] ); + + EXPECT_TRUE( rules.rulesFor( RifEclipseSummaryAddress::wellAddress( "WOPR", "A-1" ), "auto case" ).size() == 1 ); +} + +//-------------------------------------------------------------------------------------------------- +/// Registered names keep their index when new names are added +//-------------------------------------------------------------------------------------------------- +TEST( RimCurveStyleRulesTest, ItemIndicesStableWhenNamesAdded ) +{ + RimCurveStyleRules rules; + + rules.registerItemNames( D::CASE, { "B", "D" } ); + auto indices = rules.itemIndices( D::CASE ); + EXPECT_EQ( 0, indices["B"] ); + EXPECT_EQ( 1, indices["D"] ); + + rules.registerItemNames( D::CASE, { "A", "B", "C", "D" } ); + indices = rules.itemIndices( D::CASE ); + ASSERT_EQ( 4u, indices.size() ); + EXPECT_EQ( 0, indices["B"] ); + EXPECT_EQ( 1, indices["D"] ); + EXPECT_EQ( 2, indices["A"] ); + EXPECT_EQ( 3, indices["C"] ); + + EXPECT_TRUE( rules.itemIndices( D::WELL ).empty() ); + EXPECT_TRUE( rules.itemIndices( D::VECTOR ).empty() ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +TEST( RimCurveStyleRulesTest, ControlsOnlyCurvesWithValueForDimension ) +{ + RimCurveStyleRules rules; + rules.settings()->setColorDimension( D::WELL ); + rules.settings()->setSymbolDimension( D::NONE ); + + const auto wellAddress = RifEclipseSummaryAddress::wellAddress( "WOPR", "A-1" ); + const auto fieldAddress = RifEclipseSummaryAddress::fieldAddress( "FOPT" ); + + EXPECT_FALSE( rules.controlsColor( wellAddress, "case" ) ); + + rules.setUseGlobalAppearance( true ); + + EXPECT_TRUE( rules.controlsColor( wellAddress, "case" ) ); + EXPECT_FALSE( rules.controlsColor( fieldAddress, "case" ) ); + EXPECT_FALSE( rules.controlsSymbol( wellAddress, "case" ) ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +TEST( RimCurveStyleRulesTest, CopyAppearance ) +{ + RimPlotCurveAppearance source; + source.setColor( cvf::Color3f::RED ); + source.setSymbol( RiuPlotCurveSymbol::SYMBOL_DIAMOND ); + source.setLineThickness( 4 ); + source.setSymbolLabel( "label" ); + + RimPlotCurveAppearance target; + target.copyFrom( source ); + + EXPECT_TRUE( target.color() == cvf::Color3f::RED ); + EXPECT_EQ( RiuPlotCurveSymbol::SYMBOL_DIAMOND, target.symbol() ); + EXPECT_EQ( 4, target.lineThickness() ); + EXPECT_EQ( "label", target.symbolLabel() ); +} + +//-------------------------------------------------------------------------------------------------- +/// The saved appearance is restored, and not applied after the curve is deleted +//-------------------------------------------------------------------------------------------------- +TEST( RimCurveStyleRulesTest, SavedAppearanceRestore ) +{ + auto curve = new RimSummaryCurve; + curve->setColor( cvf::Color3f::RED ); + + RimSavedCurveAppearance saved; + saved.setCurve( curve ); + + curve->setColor( cvf::Color3f::BLUE ); + EXPECT_EQ( curve, saved.restore() ); + EXPECT_TRUE( curve->color() == cvf::Color3f::RED ); + + delete curve; + EXPECT_EQ( nullptr, saved.restore() ); +} diff --git a/ApplicationLibCode/UnitTests/RimCurveStyleSettings-Test.cpp b/ApplicationLibCode/UnitTests/RimCurveStyleSettings-Test.cpp new file mode 100644 index 00000000000..fda1e1d7e16 --- /dev/null +++ b/ApplicationLibCode/UnitTests/RimCurveStyleSettings-Test.cpp @@ -0,0 +1,32 @@ +#include "gtest/gtest.h" + +#include "RifEclipseSummaryAddress.h" +#include "RimCurveStyleSettings.h" + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +TEST( RimCurveStyleSettingsTest, DefaultsControlColorByCase ) +{ + RimCurveStyleSettings settings; + + EXPECT_EQ( RimCurveStyleSettings::Dimension::CASE, settings.colorDimension() ); + EXPECT_EQ( RimCurveStyleSettings::Dimension::NONE, settings.symbolDimension() ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +TEST( RimCurveStyleSettingsTest, DimensionValues ) +{ + using D = RimCurveStyleSettings::Dimension; + + const auto well = RifEclipseSummaryAddress::wellAddress( "WOPR", "A-1" ); + const auto region = RifEclipseSummaryAddress::regionAddress( "ROPR", 3 ); + + EXPECT_EQ( "A-1", RimCurveStyleSettings::dimensionValue( D::WELL, well, "case" ) ); + EXPECT_EQ( "case", RimCurveStyleSettings::dimensionValue( D::CASE, well, "case" ) ); + EXPECT_EQ( "WOPR", RimCurveStyleSettings::dimensionValue( D::VECTOR, well, "case" ) ); + EXPECT_TRUE( RimCurveStyleSettings::dimensionValue( D::REGION, well, "case" ).empty() ); + EXPECT_EQ( "3", RimCurveStyleSettings::dimensionValue( D::REGION, region, "case" ) ); +} From ba43dc16497525d310c3475c4382900e86b9bcde Mon Sep 17 00:00:00 2001 From: Magne Sjaastad Date: Mon, 19 Oct 2026 09:40:46 +0200 Subject: [PATCH 3/4] #12925: Apply global curve appearance to summary plots and ensemble curve sets --- .../RicNewSummaryEnsembleCurveSetFeature.cpp | 3 + .../Summary/RimEnsembleCurveSet.cpp | 26 ++ .../Summary/RimEnsembleCurveSet.h | 12 +- .../Summary/RimSummaryCurve.cpp | 14 ++ .../RimSummaryCurveAppearanceCalculator.cpp | 127 ++-------- .../RimSummaryCurveAppearanceCalculator.h | 15 +- .../Summary/RimSummaryPlot.cpp | 234 ++++++++++++++++++ .../ProjectDataModel/Summary/RimSummaryPlot.h | 3 + .../Summary/RimSummaryPlotSourceStepping.cpp | 18 ++ 9 files changed, 321 insertions(+), 131 deletions(-) diff --git a/ApplicationLibCode/Commands/SummaryPlotCommands/RicNewSummaryEnsembleCurveSetFeature.cpp b/ApplicationLibCode/Commands/SummaryPlotCommands/RicNewSummaryEnsembleCurveSetFeature.cpp index 133b77a62a6..7daa56433ac 100644 --- a/ApplicationLibCode/Commands/SummaryPlotCommands/RicNewSummaryEnsembleCurveSetFeature.cpp +++ b/ApplicationLibCode/Commands/SummaryPlotCommands/RicNewSummaryEnsembleCurveSetFeature.cpp @@ -112,6 +112,9 @@ RimEnsembleCurveSet* RicNewSummaryEnsembleCurveSetFeature::addCurveSet( RimSumma filter->setActive( false ); plot->ensembleCurveSetCollection()->addCurveSet( curveSet ); + + if ( plot->useGlobalCurveAppearance() ) plot->applyDefaultCurveAppearances( { curveSet } ); + return curveSet; } diff --git a/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleCurveSet.cpp b/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleCurveSet.cpp index 8b139868416..5ef0726bc96 100644 --- a/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleCurveSet.cpp +++ b/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleCurveSet.cpp @@ -32,6 +32,7 @@ #include "SummaryPlotCommands/RicSummaryPlotEditorUi.h" +#include "RimCurveStyleRules.h" #include "RimCustomObjectiveFunction.h" #include "RimCustomObjectiveFunctionCollection.h" #include "RimDeltaSummaryEnsemble.h" @@ -333,6 +334,23 @@ void RimEnsembleCurveSet::setColor( cvf::Color3f color ) computeRealizationColor(); } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimEnsembleCurveSet::setColorAndUpdateCurves( cvf::Color3f color ) +{ + setColor( color ); + updateCurveColors(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +cvf::Color3f RimEnsembleCurveSet::color() const +{ + return m_mainEnsembleColor(); +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -1415,7 +1433,15 @@ void RimEnsembleCurveSet::appendColorGroup( caf::PdmUiOrdering& uiOrdering ) if ( m_colorMode() == RimEnsembleCurveSetColorManager::ColorMode::SINGLE_COLOR ) { + auto plot = firstAncestorOrThisOfType(); + auto rules = RimProject::current() ? RimProject::current()->curveStyleRules() : nullptr; + const auto caseName = summaryEnsemble() ? summaryEnsemble()->name().toStdString() : std::string(); + const bool isColorGlobal = plot && plot->useGlobalCurveAppearance() && rules && rules->controlsColor( summaryAddressY(), caseName ); + m_mainEnsembleColor.uiCapability()->setUiReadOnly( isColorGlobal ); colorsGroup->add( &m_mainEnsembleColor ); + if ( isColorGlobal ) + RimCurveStyleRules::addGlobalAppearanceLink( *colorsGroup, + "The color is controlled by the global curve appearance." ); } else if ( m_colorMode == ColorMode::BY_ENSEMBLE_PARAM ) { diff --git a/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleCurveSet.h b/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleCurveSet.h index 1c1a53fafd4..eec24c1c9e0 100644 --- a/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleCurveSet.h +++ b/ApplicationLibCode/ProjectDataModel/Summary/RimEnsembleCurveSet.h @@ -99,11 +99,13 @@ class RimEnsembleCurveSet : public caf::PdmObject, public RimEnsembleCurveSetInt QString name() const; - bool isCurvesVisible() const; - void setColor( cvf::Color3f color ); - void setStatisticsColor( const cvf::Color3f& color ); - void enableStatisticsLables( bool enable ); - QColor mainEnsembleColor() const; + bool isCurvesVisible() const; + void setColor( cvf::Color3f color ); + void setColorAndUpdateCurves( cvf::Color3f color ); + cvf::Color3f color() const; + void setStatisticsColor( const cvf::Color3f& color ); + void enableStatisticsLables( bool enable ); + QColor mainEnsembleColor() const; void loadDataAndUpdate( bool updateParentPlot ); void setParentPlotNoReplot( RiuPlotWidget* plot ); diff --git a/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryCurve.cpp b/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryCurve.cpp index cdd09962d85..5fb5b3244b6 100644 --- a/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryCurve.cpp +++ b/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryCurve.cpp @@ -31,6 +31,7 @@ #include "Summary/RiaSummaryDefines.h" #include "Summary/RiaSummaryTools.h" +#include "RimCurveStyleRules.h" #include "RimEclipseResultCase.h" #include "RimEnsembleCurveSet.h" #include "RimEnsembleCurveSetCollection.h" @@ -893,7 +894,20 @@ void RimSummaryCurve::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering caf::PdmUiGroup* stackingGroup = uiOrdering.addNewGroup( "Stacking" ); RimStackablePlotCurve::stackingUiOrdering( *stackingGroup ); + const auto plot = firstAncestorOrThisOfType(); + const auto rules = RimProject::current() ? RimProject::current()->curveStyleRules() : nullptr; + const auto caseName = RimCurveStyleSettings::caseName( summaryCaseY() ); + + // Curves without a value for the selected dimensions are not changed by the global appearance + const bool isAppearanceGlobal = + plot && plot->useGlobalCurveAppearance() && firstAncestorOrThisOfType() && rules && + ( rules->controlsColor( summaryAddressY(), caseName ) || rules->controlsSymbol( summaryAddressY(), caseName ) ); + m_curveAppearance->setUiLocked( isAppearanceGlobal ); + caf::PdmUiGroup* appearanceGroup = uiOrdering.addNewGroup( "Appearance" ); + if ( isAppearanceGlobal ) + RimCurveStyleRules::addGlobalAppearanceLink( *appearanceGroup, + "The appearance is controlled by the global curve appearance." ); RimPlotCurve::appearanceUiOrdering( *appearanceGroup ); caf::PdmUiGroup* nameGroup = uiOrdering.addNewGroup( "Curve Name" ); diff --git a/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryCurveAppearanceCalculator.cpp b/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryCurveAppearanceCalculator.cpp index fc637045388..81b2e3aea38 100644 --- a/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryCurveAppearanceCalculator.cpp +++ b/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryCurveAppearanceCalculator.cpp @@ -22,37 +22,22 @@ #include "RiaColorTools.h" #include "RiaPreferencesSummary.h" #include "Summary/RiaSummaryCurveDefinition.h" +#include "Summary/RiaSummaryTools.h" #include "RiuQwtPlotCurve.h" +#include "RifSummaryReaderInterface.h" +#include "RimCurveStyleRules.h" #include "RimProject.h" #include "RimSummaryCase.h" #include "RimSummaryCurve.h" -#include "RifSummaryReaderInterface.h" - #include "cvfVector3.h" #include "cafAssert.h" #include -namespace caf -{ -template <> -void caf::AppEnum::setUp() -{ - addItem( RimSummaryCurveAppearanceCalculator::NONE, "NONE", "None" ); - addItem( RimSummaryCurveAppearanceCalculator::COLOR, "COLOR", "Color" ); - addItem( RimSummaryCurveAppearanceCalculator::SYMBOL, "SYMBOL", "Symbols" ); - addItem( RimSummaryCurveAppearanceCalculator::LINE_STYLE, "LINE_STYLE", "Line Style" ); - addItem( RimSummaryCurveAppearanceCalculator::GRADIENT, "GRADIENT", "Gradient" ); - addItem( RimSummaryCurveAppearanceCalculator::LINE_THICKNESS, "LINE_THICKNESS", "Line Thickness" ); - - setDefault( RimSummaryCurveAppearanceCalculator::NONE ); -} -} // namespace caf - namespace internal { //-------------------------------------------------------------------------------------------------- @@ -99,32 +84,6 @@ RimSummaryCurveAppearanceCalculator::RimSummaryCurveAppearanceCalculator( const init( curveDefVector ); } -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -void RimSummaryCurveAppearanceCalculator::assignDimensions( CurveAppearanceType caseAppearance, - CurveAppearanceType variAppearance, - CurveAppearanceType wellAppearance, - CurveAppearanceType gropAppearance, - CurveAppearanceType regiAppearance ) -{ - m_caseAppearanceType = caseAppearance; - m_varAppearanceType = variAppearance; - m_wellAppearanceType = wellAppearance; - m_groupAppearanceType = gropAppearance; - m_regionAppearanceType = regiAppearance; - - // Update the dimensionCount - m_dimensionCount = 0; - if ( m_caseAppearanceType != NONE ) ++m_dimensionCount; - if ( m_varAppearanceType != NONE ) ++m_dimensionCount; - if ( m_wellAppearanceType != NONE ) ++m_dimensionCount; - if ( m_groupAppearanceType != NONE ) ++m_dimensionCount; - if ( m_regionAppearanceType != NONE ) ++m_dimensionCount; - - updateApperanceIndices(); -} - //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -259,22 +218,6 @@ std::map RimSummaryCurveAppearanceCalculator::mapNameToAppe return nameToIndex; } -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -void RimSummaryCurveAppearanceCalculator::getDimensions( CurveAppearanceType* caseAppearance, - CurveAppearanceType* variAppearance, - CurveAppearanceType* wellAppearance, - CurveAppearanceType* gropAppearance, - CurveAppearanceType* regiAppearance ) const -{ - *caseAppearance = m_caseAppearanceType; - *variAppearance = m_varAppearanceType; - *wellAppearance = m_wellAppearanceType; - *gropAppearance = m_groupAppearanceType; - *regiAppearance = m_regionAppearanceType; -} - //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -425,8 +368,8 @@ cvf::Color3f RimSummaryCurveAppearanceCalculator::computeTintedCurveColorForAddr //-------------------------------------------------------------------------------------------------- void RimSummaryCurveAppearanceCalculator::init( const std::vector& curveDefinitions ) { - m_allSummaryCaseNames = getAllSummaryCaseNames(); - m_allSummaryWellNames = getAllSummaryWellNames(); + m_allSummaryCaseNames = RiaSummaryTools::allSummaryCaseNames(); + m_allSummaryWellNames = RiaSummaryTools::allSummaryWellNames(); for ( const RiaSummaryCurveDefinition& curveDef : curveDefinitions ) { @@ -549,6 +492,16 @@ cvf::Color3f RimSummaryCurveAppearanceCalculator::cycledPaletteColor( int colorI { if ( colorIndex < 0 ) return cvf::Color3f::BLACK; + // The palette selected for the global appearance is only used by plots using it + if ( auto project = RimProject::current() ) + { + auto rules = project->curveStyleRules(); + if ( rules && rules->useGlobalAppearance() ) + { + if ( auto legendColor = rules->autoLegendColor( colorIndex ) ) return *legendColor; + } + } + return RiaColorTables::summaryCurveDefaultPaletteColors().cycledColor3f( colorIndex ); } @@ -661,53 +614,3 @@ cvf::Color3f RimSummaryCurveAppearanceCalculator::gradeColor( const cvf::Color3f return cvf::Color3f( newColor[0], newColor[1], newColor[2] ); } - -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -std::set RimSummaryCurveAppearanceCalculator::getAllSummaryCaseNames() -{ - std::set summaryCaseHashes; - RimProject* proj = RimProject::current(); - - std::vector cases = proj->allSummaryCases(); - for ( RimSummaryCase* rimCase : cases ) - { - summaryCaseHashes.insert( rimCase->summaryHeaderFilename().toUtf8().constData() ); - } - - return summaryCaseHashes; -} - -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -std::set RimSummaryCurveAppearanceCalculator::getAllSummaryWellNames() -{ - std::set summaryWellNames; - RimProject* proj = RimProject::current(); - - std::vector cases = proj->allSummaryCases(); - for ( RimSummaryCase* rimCase : cases ) - { - RifSummaryReaderInterface* reader = nullptr; - if ( rimCase ) - { - reader = rimCase->summaryReader(); - } - - if ( reader ) - { - const std::set allAddresses = reader->allResultAddresses(); - - for ( auto& address : allAddresses ) - { - if ( address.category() == RifEclipseSummaryAddressDefines::SummaryCategory::SUMMARY_WELL ) - { - summaryWellNames.insert( address.wellName() ); - } - } - } - } - return summaryWellNames; -} diff --git a/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryCurveAppearanceCalculator.h b/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryCurveAppearanceCalculator.h index 4b98a2f7953..5af2f0a2140 100644 --- a/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryCurveAppearanceCalculator.h +++ b/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryCurveAppearanceCalculator.h @@ -36,6 +36,7 @@ class RimSummaryCurveAppearanceCalculator public: explicit RimSummaryCurveAppearanceCalculator( const std::set& curveDefinitions ); explicit RimSummaryCurveAppearanceCalculator( const std::vector& curveDefinitions ); + enum CurveAppearanceType { NONE, @@ -46,17 +47,6 @@ class RimSummaryCurveAppearanceCalculator LINE_THICKNESS }; - void assignDimensions( CurveAppearanceType caseAppearance, - CurveAppearanceType variAppearance, - CurveAppearanceType wellAppearance, - CurveAppearanceType gropAppearance, - CurveAppearanceType regiAppearance ); - void getDimensions( CurveAppearanceType* caseAppearance, - CurveAppearanceType* variAppearance, - CurveAppearanceType* wellAppearance, - CurveAppearanceType* gropAppearance, - CurveAppearanceType* regiAppearance ) const; - void setupCurveLook( RimSummaryCurve* curve ); static cvf::Color3f assignColorByPhase( const RifEclipseSummaryAddress& address ); @@ -82,9 +72,6 @@ class RimSummaryCurveAppearanceCalculator cvf::Color3f gradeColor( const cvf::Color3f& color, float factor ); void assignColorByPhase( RimSummaryCurve* curve, int colorIndex ); - static std::set getAllSummaryCaseNames(); - static std::set getAllSummaryWellNames(); - cvf::Color3f m_currentCurveBaseColor; float m_currentCurveGradient; diff --git a/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryPlot.cpp b/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryPlot.cpp index a57c121a5b8..ca4e59454dd 100644 --- a/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryPlot.cpp +++ b/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryPlot.cpp @@ -35,6 +35,9 @@ #include "Annotations/RimTimeAxisAnnotationUpdater.h" #include "RimAsciiDataCurve.h" +#include "RimCurveStyleRule.h" +#include "RimCurveStyleRules.h" +#include "RimCurveStyleSettings.h" #include "RimEnsembleCurveSet.h" #include "RimEnsembleCurveSetCollection.h" #include "RimGridTimeHistoryCurve.h" @@ -776,12 +779,179 @@ void RimSummaryPlot::applyDefaultCurveAppearances() applyDefaultCurveAppearances( ensembleCurveSetCollection()->curveSets() ); } +namespace +{ +//-------------------------------------------------------------------------------------------------- +/// Project-stored custom rules for specific items of the dimensions selected for color or symbol. Later rules win. +//-------------------------------------------------------------------------------------------------- +std::vector rulesForCurve( RimSummaryCurve* curve ) +{ + auto project = RimProject::current(); + if ( !project || !project->curveStyleRules() ) return {}; + + return project->curveStyleRules()->rulesFor( curve->summaryAddressY(), RimCurveStyleSettings::caseName( curve->summaryCaseY() ) ); +} + +//-------------------------------------------------------------------------------------------------- +/// Custom color for an ensemble curve set, if any. Later matching rules win. +//-------------------------------------------------------------------------------------------------- +std::optional overriddenEnsembleColor( RimEnsembleCurveSet* curveSet ) +{ + auto project = RimProject::current(); + if ( !project || !project->curveStyleRules() ) return std::nullopt; + + const std::string caseName = curveSet->summaryEnsemble() ? curveSet->summaryEnsemble()->name().toStdString() : std::string(); + + const auto colorDimension = project->curveStyleRules()->settings()->colorDimension(); + + std::optional color; + for ( auto rule : project->curveStyleRules()->rulesFor( curveSet->summaryAddressY(), caseName ) ) + { + if ( rule->dimension() == colorDimension && rule->overriddenColor() ) color = rule->overriddenColor(); + } + + return color; +} + +//-------------------------------------------------------------------------------------------------- +/// Maps the values of a dimension to indices used to pick palette colors and symbols. Cases and wells are +/// indexed among all items in the project to give the same item the same look in every plot, all other +/// dimensions among the values present in the plot. +//-------------------------------------------------------------------------------------------------- +class DimensionIndexer +{ +public: + using Settings = RimCurveStyleSettings; + + DimensionIndexer( Settings::Dimension dimension, const std::set& curveDefs ) + : m_dimension( dimension ) + { + if ( dimension == Settings::Dimension::NONE ) return; + + if ( dimension == Settings::Dimension::CASE || dimension == Settings::Dimension::WELL ) + { + auto rules = RimProject::current()->curveStyleRules(); + rules->registerItemNames( dimension, RimCurveStyleRule::availableNames( dimension ) ); + for ( const auto& [name, index] : rules->itemIndices( dimension ) ) + { + m_indices[name.toStdString()] = index; + } + } + else + { + std::set values; + for ( const auto& curveDef : curveDefs ) + { + auto value = Settings::dimensionValue( dimension, curveDef.summaryAddressY(), caseName( curveDef ) ); + if ( !value.empty() ) values.insert( value ); + } + + int index = 0; + for ( const auto& value : values ) + { + m_indices[value] = index++; + } + } + } + + static std::string caseName( RimSummaryCase* summaryCase ) { return Settings::caseName( summaryCase ); } + + static std::string caseName( const RiaSummaryCurveDefinition& curveDef ) + { + if ( curveDef.summaryCaseY() ) return caseName( curveDef.summaryCaseY() ); + return curveDef.ensemble() ? curveDef.ensemble()->name().toStdString() : std::string(); + } + + // -1 if the dimension has no value for the address + int index( const RifEclipseSummaryAddress& address, const std::string& caseName ) const + { + if ( m_dimension == Settings::Dimension::NONE ) return -1; + + const auto it = m_indices.find( Settings::dimensionValue( m_dimension, address, caseName ) ); + return it == m_indices.end() ? -1 : it->second; + } + + Settings::Dimension dimension() const { return m_dimension; } + +private: + Settings::Dimension m_dimension; + std::map m_indices; +}; + +//-------------------------------------------------------------------------------------------------- +/// Color from the global appearance. Vectors can be colored by phase. +//-------------------------------------------------------------------------------------------------- +std::optional globalColor( const DimensionIndexer& indexer, const RifEclipseSummaryAddress& address, const std::string& caseName ) +{ + if ( indexer.dimension() == RimCurveStyleSettings::Dimension::NONE ) return std::nullopt; + + if ( indexer.dimension() == RimCurveStyleSettings::Dimension::VECTOR && RiaPreferencesSummary::current()->colorCurvesByPhase() ) + { + return RimSummaryCurveAppearanceCalculator::assignColorByPhase( address ); + } + + const int index = indexer.index( address, caseName ); + if ( index < 0 ) return std::nullopt; + + return RimSummaryCurveAppearanceCalculator::cycledPaletteColor( index ); +} +} // namespace + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimSummaryPlot::applyDefaultCurveAppearances( std::vector curvesToUpdate ) { std::set allCurveDefs = summaryAndEnsembleCurveDefinitions(); + + const RimCurveStyleSettings* settings = nullptr; + if ( useGlobalCurveAppearance() ) + { + settings = RimProject::current()->curveStyleRules()->settings(); + + // Indices depend on all curves in the plot, so all curves must be updated to stay consistent + curvesToUpdate = summaryCurves(); + } + + if ( settings ) + { + RimCurveStyleRule::AvailableNamesCache namesCache; + + const auto rules = RimProject::current()->curveStyleRules(); + const DimensionIndexer colorIndexer( settings->colorDimension(), allCurveDefs ); + const DimensionIndexer symbolIndexer( settings->symbolDimension(), allCurveDefs ); + + for ( auto& curve : curvesToUpdate ) + { + const auto address = curve->summaryAddressY(); + const auto caseName = DimensionIndexer::caseName( curve->summaryCaseY() ); + + // Curves without a value for the selected dimensions keep their own appearance and stay editable + if ( !rules->controlsColor( address, caseName ) && !rules->controlsSymbol( address, caseName ) ) continue; + + const auto originalColor = curve->color(); + curve->resetAppearance(); + + // Curves where only the symbol is assigned globally keep their color + if ( !rules->controlsColor( address, caseName ) ) + curve->setColor( originalColor ); + else if ( auto color = globalColor( colorIndexer, address, caseName ) ) + curve->setColor( *color ); + + const int symbolIndex = symbolIndexer.index( address, caseName ); + if ( symbolIndex >= 0 ) curve->setSymbol( RimSummaryCurveAppearanceCalculator::cycledSymbol( symbolIndex ) ); + + curve->setCurveAppearanceFromCaseType(); + + for ( auto rule : rulesForCurve( curve ) ) + { + rule->applyToCurve( curve, rule->dimension() == settings->colorDimension(), rule->dimension() == settings->symbolDimension() ); + } + } + + return; + } + RimSummaryCurveAppearanceCalculator curveLookCalc( allCurveDefs ); for ( auto& curve : curvesToUpdate ) @@ -791,11 +961,58 @@ void RimSummaryPlot::applyDefaultCurveAppearances( std::vector } } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RimSummaryPlot::useGlobalCurveAppearance() const +{ + auto project = RimProject::current(); + if ( !project || !project->curveStyleRules() ) return false; + + return project->curveStyleRules()->useGlobalAppearance(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimSummaryPlot::updateCurveAppearanceFromGlobalSettings() +{ + if ( !useGlobalCurveAppearance() ) return; + + applyGlobalCurveAppearance(); + loadDataAndUpdate(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimSummaryPlot::applyGlobalCurveAppearance() +{ + if ( !useGlobalCurveAppearance() ) return; + + RimCurveStyleRule::AvailableNamesCache namesCache; + + applyDefaultCurveAppearances( summaryCurves() ); + applyDefaultCurveAppearances( ensembleCurveSetCollection()->curveSets() ); + + updateConnectedEditors(); +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimSummaryPlot::applyDefaultCurveAppearances( std::vector ensembleCurvesToUpdate ) { + // Indices depend on all curve sets in the plot, so all must be updated to stay consistent + RimCurveStyleRule::AvailableNamesCache namesCache; + std::optional globalColorIndexer; + if ( useGlobalCurveAppearance() ) + { + ensembleCurvesToUpdate = ensembleCurveSetCollection()->curveSets(); + globalColorIndexer.emplace( RimProject::current()->curveStyleRules()->settings()->colorDimension(), + summaryAndEnsembleCurveDefinitions() ); + } + std::vector usedColors; for ( auto c : ensembleCurveSetCollection()->curveSets() ) { @@ -820,7 +1037,24 @@ void RimSummaryPlot::applyDefaultCurveAppearances( std::vectorcolorMode() ) ) { + if ( globalColorIndexer ) + { + const auto caseName = curveSet->summaryEnsemble() ? curveSet->summaryEnsemble()->name().toStdString() : std::string(); + if ( auto overrideColor = overriddenEnsembleColor( curveSet ) ) + { + curveSet->setColorAndUpdateCurves( *overrideColor ); + } + else if ( auto color = globalColor( *globalColorIndexer, adr, caseName ) ) + { + curveSet->setColorAndUpdateCurves( *color ); + } + + // Curve sets without a value for the color dimension keep their own color + continue; + } + std::vector candidateColors; + if ( RiaPreferencesSummary::current()->colorCurvesByPhase() ) { // Put the the phase color as first candidate, will then be used if there is only one ensemble in the plot diff --git a/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryPlot.h b/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryPlot.h index 803a0c8254f..1bc042bc26c 100644 --- a/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryPlot.h +++ b/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryPlot.h @@ -187,6 +187,9 @@ class RimSummaryPlot : public RimPlot, public RimSummaryDataSourceStepping, publ void applyDefaultCurveAppearances(); void applyDefaultCurveAppearances( std::vector curvesToUpdate ); void applyDefaultCurveAppearances( std::vector ensembleCurvesToUpdate ); + bool useGlobalCurveAppearance() const; + void updateCurveAppearanceFromGlobalSettings(); + void applyGlobalCurveAppearance(); void setNormalizationEnabled( bool enable ); bool isNormalizationEnabled(); diff --git a/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryPlotSourceStepping.cpp b/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryPlotSourceStepping.cpp index 680aeec8381..c056612e49f 100644 --- a/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryPlotSourceStepping.cpp +++ b/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryPlotSourceStepping.cpp @@ -549,6 +549,24 @@ void RimSummaryPlotSourceStepping::fieldChangedByUi( const caf::PdmFieldHandle* if ( triggerLoadDataAndUpdate ) { + // Plots using global curve appearance must be restyled after the stepped addresses and cases are updated + std::set plotsToRestyle; + for ( auto curve : curves ) + { + if ( auto plot = curve->firstAncestorOfType() ) plotsToRestyle.insert( plot ); + } + if ( dataSourceSteppingObject() ) + { + for ( auto curveSet : dataSourceSteppingObject()->curveSets() ) + { + if ( auto plot = curveSet->firstAncestorOfType() ) plotsToRestyle.insert( plot ); + } + } + for ( auto plot : plotsToRestyle ) + { + plot->applyGlobalCurveAppearance(); + } + RimSummaryMultiPlot* summaryMultiPlot = dynamic_cast( m_objectForSourceStepping.p() ); if ( summaryMultiPlot ) { From d04e1090f3f641b50b290e292cd62304cc8be47b Mon Sep 17 00:00:00 2001 From: Magne Sjaastad Date: Mon, 19 Oct 2026 09:40:46 +0200 Subject: [PATCH 4/4] #12925: Add Curve Appearance dock window --- .../UserInterface/RiuDockWidgetTools.cpp | 10 +++++ .../UserInterface/RiuDockWidgetTools.h | 1 + .../UserInterface/RiuPlotMainWindow.cpp | 38 +++++++++++++++++++ .../UserInterface/RiuPlotMainWindow.h | 2 + 4 files changed, 51 insertions(+) diff --git a/ApplicationLibCode/UserInterface/RiuDockWidgetTools.cpp b/ApplicationLibCode/UserInterface/RiuDockWidgetTools.cpp index faf5df78541..9b0a1852014 100644 --- a/ApplicationLibCode/UserInterface/RiuDockWidgetTools.cpp +++ b/ApplicationLibCode/UserInterface/RiuDockWidgetTools.cpp @@ -175,6 +175,14 @@ QString RiuDockWidgetTools::plotMainWindowPlotManagerName() return "dockSummaryPlotManager"; } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RiuDockWidgetTools::plotMainWindowCurveAppearanceName() +{ + return "dockCurveAppearance"; +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -419,6 +427,8 @@ QIcon RiuDockWidgetTools::dockIcon( const QString dockWidgetName ) return QIcon( ":/undo-stack.svg" ); else if ( dockWidgetName == plotMainWindowPlotManagerName() ) return QIcon( ":/plot-manager.svg" ); + else if ( dockWidgetName == plotMainWindowCurveAppearanceName() ) + return QIcon( ":/property-editor.svg" ); else if ( dockWidgetName == mainWindowPropertyEditorName() ) return QIcon( ":/property-editor.svg" ); else if ( dockWidgetName == mainWindowResultInfoName() ) diff --git a/ApplicationLibCode/UserInterface/RiuDockWidgetTools.h b/ApplicationLibCode/UserInterface/RiuDockWidgetTools.h index 59b47268cae..9ab9266a45e 100644 --- a/ApplicationLibCode/UserInterface/RiuDockWidgetTools.h +++ b/ApplicationLibCode/UserInterface/RiuDockWidgetTools.h @@ -83,6 +83,7 @@ class RiuDockWidgetTools static QString plotMainWindowMessagesName(); static QString plotMainWindowUndoStackName(); static QString plotMainWindowPlotManagerName(); + static QString plotMainWindowCurveAppearanceName(); static QString plotWindowQuickAccessName(); static QString dockState3DEclipseName(); diff --git a/ApplicationLibCode/UserInterface/RiuPlotMainWindow.cpp b/ApplicationLibCode/UserInterface/RiuPlotMainWindow.cpp index f120685e545..2cda56ae52a 100644 --- a/ApplicationLibCode/UserInterface/RiuPlotMainWindow.cpp +++ b/ApplicationLibCode/UserInterface/RiuPlotMainWindow.cpp @@ -29,6 +29,7 @@ #include "Histogram/RimHistogramMultiPlot.h" #include "Histogram/RimHistogramPlot.h" +#include "RimCurveStyleRules.h" #include "RimEnsembleCurveSetCollection.h" #include "RimMainPlotCollection.h" #include "RimMultiPlot.h" @@ -78,6 +79,7 @@ #include "DockManager.h" #include +#include #include #include #include @@ -125,6 +127,7 @@ RiuPlotMainWindow::~RiuPlotMainWindow() setBeingDestroyed(); m_summaryPlotManagerView->showProperties( nullptr ); + m_curveStyleRulesView->showProperties( nullptr ); setPdmRoot( nullptr ); } @@ -169,6 +172,8 @@ void RiuPlotMainWindow::initializeGuiNewProjectLoaded() setPdmRoot( RimProject::current() ); restoreTreeViewState(); + m_curveStyleRulesView->showProperties( RimProject::current()->curveStyleRules() ); + RimMainPlotCollection* mainPlotColl = RimMainPlotCollection::current(); mainPlotColl->ensureDefaultFlowPlotsAreCreated(); @@ -242,6 +247,7 @@ void RiuPlotMainWindow::cleanupGuiBeforeProjectClose() { pdmUiPropertyView->showProperties( nullptr ); } + m_curveStyleRulesView->showProperties( nullptr ); cleanUpTemporaryWidgets(); @@ -592,6 +598,16 @@ void RiuPlotMainWindow::createDockPanels() rightWidgets.push_back( dockWidget ); } + // the global curve appearance and per item rules, stored in the project + { + auto dockWidget = + RiuDockWidgetTools::createDockWidget( "Curve Appearance", RiuDockWidgetTools::plotMainWindowCurveAppearanceName(), dockManager() ); + + m_curveStyleRulesView = std::make_unique( dockWidget ); + dockWidget->setWidget( m_curveStyleRulesView.get() ); + rightWidgets.push_back( dockWidget ); + } + // the undo stack if ( m_undoView && RiaPreferences::current()->useUndoRedo() ) { @@ -766,6 +782,28 @@ RiuMessagePanel* RiuPlotMainWindow::messagePanel() return m_messagePanel; } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RiuPlotMainWindow::showCurveAppearance() +{ + auto dockWidget = RiuDockWidgetTools::findDockWidget( dockManager(), RiuDockWidgetTools::plotMainWindowCurveAppearanceName() ); + if ( !dockWidget ) return; + + dockWidget->toggleView( true ); + dockWidget->raise(); + dockWidget->setAsCurrentTab(); + + // Briefly outline the dock edges so the user sees where the settings are, also when it was already visible + auto outline = new QFrame( dockWidget ); + outline->setAttribute( Qt::WA_TransparentForMouseEvents ); + outline->setStyleSheet( "QFrame { background: transparent; border: 3px solid rgb(255, 160, 0); }" ); + outline->setGeometry( dockWidget->rect() ); + outline->show(); + outline->raise(); + QTimer::singleShot( 1000, outline, &QObject::deleteLater ); +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/UserInterface/RiuPlotMainWindow.h b/ApplicationLibCode/UserInterface/RiuPlotMainWindow.h index 5a319643219..4aa5617c331 100644 --- a/ApplicationLibCode/UserInterface/RiuPlotMainWindow.h +++ b/ApplicationLibCode/UserInterface/RiuPlotMainWindow.h @@ -84,6 +84,7 @@ class RiuPlotMainWindow : public RiuMainWindowBase RiuMessagePanel* messagePanel(); void showAndSetKeyboardFocusToSummaryPlotManager(); + void showCurveAppearance(); protected: void closeEvent( QCloseEvent* event ) override; @@ -130,6 +131,7 @@ private slots: std::unique_ptr m_multiPlotLayoutToolBarEditor; std::unique_ptr m_summaryPlotManagerView; + std::unique_ptr m_curveStyleRulesView; std::unique_ptr m_summaryCurveCreatorDialog; std::unique_ptr m_summaryCurveCalculatorDialog;