From 52f9cdf777fcc572354859306dbce694b69f0343 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Herje?= <82032112+jorgenherje@users.noreply.github.com> Date: Wed, 30 Sep 2026 18:16:00 +0200 Subject: [PATCH] Add Polygons data from Cloud (#14811) Add fetch og polygons from cloud using ri-cloud-api service as submodule Current version of submodule fetches polygons from Sumo, prodviding polygons with standard results tag. --- ApplicationExeCode/Resources/Download.svg | 11 + ApplicationExeCode/Resources/ResInsight.qrc | 1 + .../Tools/Cloud/CMakeLists_files.cmake | 1 + .../Tools/Cloud/RiaCloudApiService.cpp | 14 + .../Tools/Cloud/RiaSumoConnector.cpp | 9 + .../Tools/Cloud/RiaSumoConnector.h | 11 +- .../Tools/Cloud/RiaSumoPolygons.cpp | 162 +++++++ .../Application/Tools/Cloud/RiaSumoPolygons.h | 94 ++++ ApplicationLibCode/CMakeLists.txt | 1 + .../PolygonCommands/CMakeLists_files.cmake | 2 + .../RicAddCloudPolygonSourceFeature.cpp | 81 ++++ .../RicAddCloudPolygonSourceFeature.h | 34 ++ .../RicReloadPolygonCloudAddressFeature.cpp | 65 +++ .../RicReloadPolygonCloudAddressFeature.h | 33 ++ .../Polygons/Cloud/CMakeLists_files.cmake | 8 + .../Polygons/Cloud/RimCloudPolygon.cpp | 108 +++++ .../Polygons/Cloud/RimCloudPolygon.h | 64 +++ .../Polygons/Cloud/RimPolygonCloudAddress.cpp | 456 ++++++++++++++++++ .../Polygons/Cloud/RimPolygonCloudAddress.h | 114 +++++ .../Polygons/Cloud/RimPolygonCloudFolder.cpp | 64 +++ .../Polygons/Cloud/RimPolygonCloudFolder.h | 40 ++ .../Polygons/Cloud/RimPolygonCloudSource.cpp | 447 +++++++++++++++++ .../Polygons/Cloud/RimPolygonCloudSource.h | 111 +++++ .../Polygons/RimPolygonCollection.cpp | 83 ++++ .../Polygons/RimPolygonCollection.h | 15 + .../Polygons/RimPolygonContainer.cpp | 25 + .../Polygons/RimPolygonContainer.h | 24 +- .../Polygons/RimPolygonInViewCollection.cpp | 211 +++++++- .../Polygons/RimPolygonInViewCollection.h | 38 ++ .../RimNestedMirrorCollectionInView.h | 13 + .../RimNestedMirrorCollectionInView.inl | 42 +- .../ProjectDataModel/RimRoffCaseSumo.cpp | 8 + .../ProjectDataModel/RimRoffCaseSumo.h | 5 + .../cafPdmUiCore/CMakeLists.txt | 2 + .../cafPdmUiCore/cafPdmUiButtonBox.cpp | 76 +++ .../cafPdmUiCore/cafPdmUiButtonBox.h | 80 +++ .../cafPdmUiCore/cafPdmUiGroup.cpp | 17 + .../cafPdmUiCore/cafPdmUiGroup.h | 5 + .../cafPdmUiCore/cafPdmUiOrdering.cpp | 14 + .../cafPdmUiCore/cafPdmUiOrdering.h | 23 +- .../cafPdmUiFormLayoutObjectEditor.cpp | 54 ++- .../cafPdmUiFormLayoutObjectEditor.h | 14 +- .../cafUserInterface/cafPdmUiTreeAttributes.h | 1 + .../cafPdmUiTreeViewItemDelegate.cpp | 24 + .../cafPdmUiTreeViewItemDelegate.h | 5 + scripts/ri-cloud-api | 2 +- 46 files changed, 2674 insertions(+), 38 deletions(-) create mode 100644 ApplicationExeCode/Resources/Download.svg create mode 100644 ApplicationLibCode/Application/Tools/Cloud/RiaSumoPolygons.cpp create mode 100644 ApplicationLibCode/Application/Tools/Cloud/RiaSumoPolygons.h create mode 100644 ApplicationLibCode/Commands/PolygonCommands/RicAddCloudPolygonSourceFeature.cpp create mode 100644 ApplicationLibCode/Commands/PolygonCommands/RicAddCloudPolygonSourceFeature.h create mode 100644 ApplicationLibCode/Commands/PolygonCommands/RicReloadPolygonCloudAddressFeature.cpp create mode 100644 ApplicationLibCode/Commands/PolygonCommands/RicReloadPolygonCloudAddressFeature.h create mode 100644 ApplicationLibCode/ProjectDataModel/Polygons/Cloud/CMakeLists_files.cmake create mode 100644 ApplicationLibCode/ProjectDataModel/Polygons/Cloud/RimCloudPolygon.cpp create mode 100644 ApplicationLibCode/ProjectDataModel/Polygons/Cloud/RimCloudPolygon.h create mode 100644 ApplicationLibCode/ProjectDataModel/Polygons/Cloud/RimPolygonCloudAddress.cpp create mode 100644 ApplicationLibCode/ProjectDataModel/Polygons/Cloud/RimPolygonCloudAddress.h create mode 100644 ApplicationLibCode/ProjectDataModel/Polygons/Cloud/RimPolygonCloudFolder.cpp create mode 100644 ApplicationLibCode/ProjectDataModel/Polygons/Cloud/RimPolygonCloudFolder.h create mode 100644 ApplicationLibCode/ProjectDataModel/Polygons/Cloud/RimPolygonCloudSource.cpp create mode 100644 ApplicationLibCode/ProjectDataModel/Polygons/Cloud/RimPolygonCloudSource.h create mode 100644 Fwk/AppFwk/cafProjectDataModel/cafPdmUiCore/cafPdmUiButtonBox.cpp create mode 100644 Fwk/AppFwk/cafProjectDataModel/cafPdmUiCore/cafPdmUiButtonBox.h diff --git a/ApplicationExeCode/Resources/Download.svg b/ApplicationExeCode/Resources/Download.svg new file mode 100644 index 0000000000..49f95179e8 --- /dev/null +++ b/ApplicationExeCode/Resources/Download.svg @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/ApplicationExeCode/Resources/ResInsight.qrc b/ApplicationExeCode/Resources/ResInsight.qrc index 8d214434cc..8ba437ff64 100644 --- a/ApplicationExeCode/Resources/ResInsight.qrc +++ b/ApplicationExeCode/Resources/ResInsight.qrc @@ -237,6 +237,7 @@ Erase.svg Close.svg Refresh.svg + Download.svg CellFilter.png FilterFunction.svg FilterParameter.svg diff --git a/ApplicationLibCode/Application/Tools/Cloud/CMakeLists_files.cmake b/ApplicationLibCode/Application/Tools/Cloud/CMakeLists_files.cmake index b34b6e5153..d45aeb8295 100644 --- a/ApplicationLibCode/Application/Tools/Cloud/CMakeLists_files.cmake +++ b/ApplicationLibCode/Application/Tools/Cloud/CMakeLists_files.cmake @@ -6,6 +6,7 @@ set(SOURCE_GROUP_SOURCE_FILES ${CMAKE_CURRENT_LIST_DIR}/RiaSumoExplore.cpp ${CMAKE_CURRENT_LIST_DIR}/RiaSumoGrid.cpp ${CMAKE_CURRENT_LIST_DIR}/RiaSumoSummary.cpp + ${CMAKE_CURRENT_LIST_DIR}/RiaSumoPolygons.cpp ${CMAKE_CURRENT_LIST_DIR}/RiaConnectorTools.cpp ${CMAKE_CURRENT_LIST_DIR}/RiaOsduConnector.cpp ${CMAKE_CURRENT_LIST_DIR}/RiaOAuthHttpServerReplyHandler.cpp diff --git a/ApplicationLibCode/Application/Tools/Cloud/RiaCloudApiService.cpp b/ApplicationLibCode/Application/Tools/Cloud/RiaCloudApiService.cpp index 99f7bc927a..b4cf06269f 100644 --- a/ApplicationLibCode/Application/Tools/Cloud/RiaCloudApiService.cpp +++ b/ApplicationLibCode/Application/Tools/Cloud/RiaCloudApiService.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -75,6 +76,10 @@ RiaCloudApiService::RiaCloudApiService( const QString& serverAddress, int wanted , m_consecutiveFailures( 0 ) , m_isResponding( false ) { + // Never route loopback requests through a system/corporate proxy, which can add several seconds + // per request. + m_networkAccessManager->setProxy( QNetworkProxy::NoProxy ); + m_healthTimer.setInterval( healthCheckIntervalMs ); connect( &m_healthTimer, &QTimer::timeout, this, &RiaCloudApiService::onHealthCheck ); @@ -245,6 +250,11 @@ bool RiaCloudApiService::waitUntilResponding( int timeoutMs ) if ( !isRunning() ) start(); + // Nested event loops below let the periodic health check fire and restart() the service while this + // call is still waiting for it to boot. Stop both timers for the duration of the wait to avoid that. + m_startupTimer.stop(); + m_healthTimer.stop(); + QElapsedTimer elapsed; elapsed.start(); @@ -297,6 +307,10 @@ bool RiaCloudApiService::waitUntilResponding( int timeoutMs ) RiaLogging::error( std::format( "Cloud API service: not responding after {} ms, giving up.", elapsed.elapsed() ) ); + // Resume background health-checking if the process is still running, so it can still recover on + // its own without another blocking wait. + if ( isRunning() ) m_healthTimer.start(); + return false; } diff --git a/ApplicationLibCode/Application/Tools/Cloud/RiaSumoConnector.cpp b/ApplicationLibCode/Application/Tools/Cloud/RiaSumoConnector.cpp index a88dc2875e..fdba4fa041 100644 --- a/ApplicationLibCode/Application/Tools/Cloud/RiaSumoConnector.cpp +++ b/ApplicationLibCode/Application/Tools/Cloud/RiaSumoConnector.cpp @@ -55,6 +55,7 @@ RiaSumoConnector::RiaSumoConnector( QObject* parent, , m_explore( *this ) , m_grid( *this ) , m_summary( *this ) + , m_polygons( *this ) { // The transfer thread runs the network requests issued by the blocking wrappers, so the calling thread can // wait for them without dispatching events. The context object gives us something with transfer thread @@ -102,6 +103,14 @@ RiaSumoSummary& RiaSumoConnector::summary() return m_summary; } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RiaSumoPolygons& RiaSumoConnector::polygons() +{ + return m_polygons; +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/Application/Tools/Cloud/RiaSumoConnector.h b/ApplicationLibCode/Application/Tools/Cloud/RiaSumoConnector.h index 0ceed3c88e..fbfd6f6ce9 100644 --- a/ApplicationLibCode/Application/Tools/Cloud/RiaSumoConnector.h +++ b/ApplicationLibCode/Application/Tools/Cloud/RiaSumoConnector.h @@ -22,6 +22,7 @@ #include "RiaSumoDefines.h" #include "RiaSumoExplore.h" #include "RiaSumoGrid.h" +#include "RiaSumoPolygons.h" #include "RiaSumoSummary.h" #include @@ -75,6 +76,9 @@ public: // The summary data of a case. RiaSumoSummary& summary(); + // The polygon results of an ensemble. + RiaSumoPolygons& polygons(); + // Transport used by the data specific delegates. Every request goes through the transfer thread, so // the calling thread waits without dispatching events. // Issue a GET against the local cloud API service and return the response body. Takes the path only, @@ -179,9 +183,10 @@ private: private: std::function m_serverUrlProvider; - RiaSumoExplore m_explore; - RiaSumoGrid m_grid; - RiaSumoSummary m_summary; + RiaSumoExplore m_explore; + RiaSumoGrid m_grid; + RiaSumoSummary m_summary; + RiaSumoPolygons m_polygons; // Transfers run on their own thread so the calling thread can wait without dispatching events. Waiting on // a nested event loop on the GUI thread let the view update code re-enter a load that was still running, diff --git a/ApplicationLibCode/Application/Tools/Cloud/RiaSumoPolygons.cpp b/ApplicationLibCode/Application/Tools/Cloud/RiaSumoPolygons.cpp new file mode 100644 index 0000000000..7c18f5a8b2 --- /dev/null +++ b/ApplicationLibCode/Application/Tools/Cloud/RiaSumoPolygons.cpp @@ -0,0 +1,162 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 "RiaSumoPolygons.h" + +#include "RiaLogging.h" +#include "RiaSumoConnector.h" + +#include +#include +#include +#include +#include + +#include + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RiaSumoPolygons::RiaSumoPolygons( RiaSumoConnector& connector ) + : m_connector( connector ) +{ +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +SumoPolygonDirectory RiaSumoPolygons::polygonResultDirectory( const SumoCaseId& caseId, const QString& ensembleName ) +{ + const QString encodedEnsembleName = QUrl::toPercentEncoding( ensembleName ); + const QString path = QString( "/cases/%1/ensembles/%2/polygon_result_directory" ).arg( caseId.get() ).arg( encodedEnsembleName ); + + return parseDirectory( m_connector.getBlocking( path, "Loading polygon result directory from Sumo" ) ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector RiaSumoPolygons::polygonsData( const SumoCaseId& caseId, + const QString& ensembleName, + int realization, + SumoPolygonResult polygonResult, + const QString& name, + const QString& contactType ) +{ + const QString encodedEnsembleName = QUrl::toPercentEncoding( ensembleName ); + + QUrlQuery query; + query.addQueryItem( "realization", QString::number( realization ) ); + query.addQueryItem( "polygon_result", polygonResultKey( polygonResult ) ); + if ( polygonResult != SumoPolygonResult::FieldOutline && !name.isEmpty() ) + { + query.addQueryItem( "name", name ); + } + if ( polygonResult == SumoPolygonResult::FluidContactOutline && !contactType.isEmpty() ) + { + query.addQueryItem( "contact_type", contactType ); + } + + const QString path = + QString( "/cases/%1/ensembles/%2/polygons_data?%3" ).arg( caseId.get() ).arg( encodedEnsembleName ).arg( query.toString( QUrl::FullyEncoded ) ); + + return parsePolygonsData( m_connector.getBlocking( path, "Loading polygon data from Sumo" ) ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RiaSumoPolygons::polygonResultKey( SumoPolygonResult polygonResult ) +{ + switch ( polygonResult ) + { + case SumoPolygonResult::FieldOutline: + return "field_outline"; + case SumoPolygonResult::StructureDepthFaultLines: + return "structure_depth_fault_lines"; + case SumoPolygonResult::FluidContactOutline: + return "fluid_contact_outline"; + } + + return {}; +} + +//-------------------------------------------------------------------------------------------------- +/// Parses the polygon_result_directory response: a JSON object keyed by category, each value a list of +/// { name } or { name, contactType } objects. A category with no matches is omitted by the API. +//-------------------------------------------------------------------------------------------------- +SumoPolygonDirectory RiaSumoPolygons::parseDirectory( const QByteArray& body ) +{ + SumoPolygonDirectory directory; + + QJsonDocument doc = QJsonDocument::fromJson( body ); + QJsonObject obj = doc.object(); + + auto parseMetaArray = []( const QJsonArray& array ) + { + std::vector result; + for ( const QJsonValue& value : array ) + { + QJsonObject metaObj = value.toObject(); + result.push_back( SumoPolygonMeta{ metaObj["name"].toString(), metaObj["contactType"].toString() } ); + } + return result; + }; + + directory.fieldOutline = parseMetaArray( obj[polygonResultKey( SumoPolygonResult::FieldOutline )].toArray() ); + directory.structureDepthFaultLines = parseMetaArray( obj[polygonResultKey( SumoPolygonResult::StructureDepthFaultLines )].toArray() ); + directory.fluidContactOutline = parseMetaArray( obj[polygonResultKey( SumoPolygonResult::FluidContactOutline )].toArray() ); + + return directory; +} + +//-------------------------------------------------------------------------------------------------- +/// Parses the polygons_data response: a JSON array of { xUtmEArr, yUtmNArray, zTvdSSArray, polyId, name }. +//-------------------------------------------------------------------------------------------------- +std::vector RiaSumoPolygons::parsePolygonsData( const QByteArray& body ) +{ + std::vector polygonDataList; + + QJsonDocument doc = QJsonDocument::fromJson( body ); + QJsonArray jsonArray = doc.array(); + + for ( const QJsonValue& value : jsonArray ) + { + QJsonObject obj = value.toObject(); + + SumoPolygonData data; + data.name = obj["name"].toString(); + + // polyId can be an int or a string on the wire; keep it as a string either way. + const QJsonValue polyIdValue = obj["polyId"]; + data.polyId = polyIdValue.isString() ? polyIdValue.toString() : QString::number( polyIdValue.toInt() ); + + for ( const QJsonValue& x : obj["xUtmEArr"].toArray() ) + data.xArr.push_back( x.toDouble() ); + for ( const QJsonValue& y : obj["yUtmNArray"].toArray() ) + data.yArr.push_back( y.toDouble() ); + for ( const QJsonValue& z : obj["zTvdSSArray"].toArray() ) + data.zArr.push_back( z.toDouble() ); + + polygonDataList.push_back( data ); + } + + RiaLogging::debug( std::format( "Polygon data count : {}", polygonDataList.size() ) ); + + return polygonDataList; +} diff --git a/ApplicationLibCode/Application/Tools/Cloud/RiaSumoPolygons.h b/ApplicationLibCode/Application/Tools/Cloud/RiaSumoPolygons.h new file mode 100644 index 0000000000..972bd8a2d8 --- /dev/null +++ b/ApplicationLibCode/Application/Tools/Cloud/RiaSumoPolygons.h @@ -0,0 +1,94 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 "RiaSumoDefines.h" + +#include +#include + +#include + +class RiaSumoConnector; + +// Category of a polygon result, matching ri-cloud-api's PolygonResult enum. +enum class SumoPolygonResult +{ + FieldOutline, + StructureDepthFaultLines, + FluidContactOutline +}; + +// One named polygon result. contactType is set only for fluid contact outline entries. +struct SumoPolygonMeta +{ + QString name; + QString contactType; +}; + +// Polygon results available for an ensemble, categorized as ri-cloud-api reports them. An absent +// category is left empty. +struct SumoPolygonDirectory +{ + std::vector fieldOutline; + std::vector structureDepthFaultLines; + std::vector fluidContactOutline; +}; + +// Decoded geometry of one polygon (one POLY_ID group), already in [x,y,z] form -- ri-cloud-api +// owns the Sumo schema and returns plain coordinate arrays. +struct SumoPolygonData +{ + QString name; + QString polyId; + std::vector xArr; + std::vector yArr; + std::vector zArr; +}; + +//================================================================================================== +/// Polygon results of a Sumo ensemble: what is available (polygon_result_directory) and decoded +/// coordinates of a chosen one (polygons_data). Both endpoints return plain JSON, so there is no +/// blob id/download step, unlike grid and summary data. +//================================================================================================== +class RiaSumoPolygons +{ +public: + explicit RiaSumoPolygons( RiaSumoConnector& connector ); + + SumoPolygonDirectory polygonResultDirectory( const SumoCaseId& caseId, const QString& ensembleName ); + + // name is ignored for FieldOutline. contactType is required only for FluidContactOutline. + std::vector polygonsData( const SumoCaseId& caseId, + const QString& ensembleName, + int realization, + SumoPolygonResult polygonResult, + const QString& name = QString(), + const QString& contactType = QString() ); + + // Query-parameter/JSON-key spelling of a polygon result category. + static QString polygonResultKey( SumoPolygonResult polygonResult ); + +private: + static SumoPolygonDirectory parseDirectory( const QByteArray& body ); + static std::vector parsePolygonsData( const QByteArray& body ); + +private: + RiaSumoConnector& m_connector; +}; diff --git a/ApplicationLibCode/CMakeLists.txt b/ApplicationLibCode/CMakeLists.txt index 1d8a2f7f96..58dccbac4f 100644 --- a/ApplicationLibCode/CMakeLists.txt +++ b/ApplicationLibCode/CMakeLists.txt @@ -127,6 +127,7 @@ list( ProjectDataModel/CellFilters/CMakeLists_files.cmake ProjectDataModel/ProcessControl/CMakeLists_files.cmake ProjectDataModel/Polygons/CMakeLists_files.cmake + ProjectDataModel/Polygons/Cloud/CMakeLists_files.cmake ProjectDataModel/WellLog/CMakeLists_files.cmake ProjectDataModel/WellLog/WellLogTrack/CMakeLists_files.cmake ProjectDataModel/WellMeasurement/CMakeLists_files.cmake diff --git a/ApplicationLibCode/Commands/PolygonCommands/CMakeLists_files.cmake b/ApplicationLibCode/Commands/PolygonCommands/CMakeLists_files.cmake index 3a8903c349..48d73ccd2c 100644 --- a/ApplicationLibCode/Commands/PolygonCommands/CMakeLists_files.cmake +++ b/ApplicationLibCode/Commands/PolygonCommands/CMakeLists_files.cmake @@ -2,6 +2,8 @@ set(SOURCE_GROUP_SOURCE_FILES ${CMAKE_CURRENT_LIST_DIR}/RicCreatePolygonFeature.cpp ${CMAKE_CURRENT_LIST_DIR}/RicImportPolygonFileFeature.cpp ${CMAKE_CURRENT_LIST_DIR}/RicReloadPolygonFileFeature.cpp + ${CMAKE_CURRENT_LIST_DIR}/RicAddCloudPolygonSourceFeature.cpp + ${CMAKE_CURRENT_LIST_DIR}/RicReloadPolygonCloudAddressFeature.cpp ${CMAKE_CURRENT_LIST_DIR}/RicDuplicatePolygonFeature.cpp ${CMAKE_CURRENT_LIST_DIR}/RicExportPolygonCsvFeature.cpp ${CMAKE_CURRENT_LIST_DIR}/RicExportPolygonPolFeature.cpp diff --git a/ApplicationLibCode/Commands/PolygonCommands/RicAddCloudPolygonSourceFeature.cpp b/ApplicationLibCode/Commands/PolygonCommands/RicAddCloudPolygonSourceFeature.cpp new file mode 100644 index 0000000000..6934a1e6e9 --- /dev/null +++ b/ApplicationLibCode/Commands/PolygonCommands/RicAddCloudPolygonSourceFeature.cpp @@ -0,0 +1,81 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// 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 "RicAddCloudPolygonSourceFeature.h" + +#include "RiaLogging.h" + +#include "Cloud/RimCloudDataSourceCollection.h" +#include "Cloud/RimSumoDataSource.h" + +#include "Polygons/Cloud/RimPolygonCloudSource.h" +#include "Polygons/RimPolygonCollection.h" +#include "RimTools.h" + +#include "Riu3DMainWindowTools.h" + +#include "cafSelectionManagerTools.h" + +#include + +CAF_CMD_SOURCE_INIT( RicAddCloudPolygonSourceFeature, "RicAddCloudPolygonSourceFeature" ); + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RicAddCloudPolygonSourceFeature::onActionTriggered( bool isChecked ) +{ + RimPolygonCollection* polygonCollection = nullptr; + + auto selected = caf::selectedObjectsByTypeStrict(); + if ( !selected.empty() ) polygonCollection = selected.front(); + + if ( !polygonCollection ) polygonCollection = RimTools::polygonCollection(); + if ( !polygonCollection ) return; + + auto dataSources = RimCloudDataSourceCollection::instance()->sumoDataSources(); + if ( dataSources.empty() ) + { + RiaLogging::warning( "No Sumo data sources configured. Add one before adding a cloud polygon source." ); + return; + } + + // Preselect the first Data Source + Realization as a convenience default (Apply is still + // required to fetch the directory and build the tree). + auto* source = new RimPolygonCloudSource(); + + source->setDataSource( dataSources.front() ); + auto realizations = source->availableRealizationIdsForOverride(); + if ( !realizations.empty() ) source->setBaseRealization( realizations.front() ); + + polygonCollection->addPolygonCloudSource( source ); + + polygonCollection->uiCapability()->updateAllRequiredEditors(); + + Riu3DMainWindowTools::setExpanded( source ); + Riu3DMainWindowTools::selectAsCurrentItem( source ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RicAddCloudPolygonSourceFeature::setupActionLook( QAction* actionToSetup ) +{ + actionToSetup->setText( "Add Cloud Polygon Source" ); + actionToSetup->setIcon( QIcon( ":/PolylinesFromFile16x16.png" ) ); +} diff --git a/ApplicationLibCode/Commands/PolygonCommands/RicAddCloudPolygonSourceFeature.h b/ApplicationLibCode/Commands/PolygonCommands/RicAddCloudPolygonSourceFeature.h new file mode 100644 index 0000000000..56e3f68a7a --- /dev/null +++ b/ApplicationLibCode/Commands/PolygonCommands/RicAddCloudPolygonSourceFeature.h @@ -0,0 +1,34 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// 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 "cafCmdFeature.h" + +//================================================================================================== +/// "Add Cloud Polygon Source": creates a RimPolygonCloudSource with a preselected Sumo data +/// source and base realization; the user clicks "Apply" to build the browsable directory tree. +//================================================================================================== +class RicAddCloudPolygonSourceFeature : public caf::CmdFeature +{ + CAF_CMD_HEADER_INIT; + +protected: + void onActionTriggered( bool isChecked ) override; + void setupActionLook( QAction* actionToSetup ) override; +}; diff --git a/ApplicationLibCode/Commands/PolygonCommands/RicReloadPolygonCloudAddressFeature.cpp b/ApplicationLibCode/Commands/PolygonCommands/RicReloadPolygonCloudAddressFeature.cpp new file mode 100644 index 0000000000..34ce67dacd --- /dev/null +++ b/ApplicationLibCode/Commands/PolygonCommands/RicReloadPolygonCloudAddressFeature.cpp @@ -0,0 +1,65 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2025 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 "RicReloadPolygonCloudAddressFeature.h" + +#include "Polygons/Cloud/RimPolygonCloudAddress.h" + +#include "cafSelectionManager.h" +#include "cafSelectionManagerTools.h" + +#include + +CAF_CMD_SOURCE_INIT( RicReloadPolygonCloudAddressFeature, "RicReloadPolygonCloudAddressFeature" ); + +//-------------------------------------------------------------------------------------------------- +/// Reloads whatever is currently visualized for the selected address(es): re-fetches base data +/// and any cached realizations. Does not rebuild the owning source's directory tree. +//-------------------------------------------------------------------------------------------------- +void RicReloadPolygonCloudAddressFeature::onActionTriggered( bool isChecked ) +{ + auto cloudAddresses = caf::selectedObjectsByType(); + + for ( auto address : cloudAddresses ) + { + std::vector cachedRealizations = address->cachedRealizations(); + + if ( address->hasBaseData() ) + { + address->evictBaseData(); + address->ensureBaseFetched(); + } + + for ( int realization : cachedRealizations ) + { + address->evictRealizationIfUnused( realization ); + address->ensureRealizationFetched( realization ); + } + + address->updateAllRequiredEditors(); + } +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RicReloadPolygonCloudAddressFeature::setupActionLook( QAction* actionToSetup ) +{ + actionToSetup->setText( "Reload" ); + actionToSetup->setIcon( QIcon( ":/Refresh.svg" ) ); +} diff --git a/ApplicationLibCode/Commands/PolygonCommands/RicReloadPolygonCloudAddressFeature.h b/ApplicationLibCode/Commands/PolygonCommands/RicReloadPolygonCloudAddressFeature.h new file mode 100644 index 0000000000..b4f8f01fc6 --- /dev/null +++ b/ApplicationLibCode/Commands/PolygonCommands/RicReloadPolygonCloudAddressFeature.h @@ -0,0 +1,33 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2025 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 "cafCmdFeature.h" + +//================================================================================================== +/// +//================================================================================================== +class RicReloadPolygonCloudAddressFeature : public caf::CmdFeature +{ + CAF_CMD_HEADER_INIT; + +protected: + void onActionTriggered( bool isChecked ) override; + void setupActionLook( QAction* actionToSetup ) override; +}; diff --git a/ApplicationLibCode/ProjectDataModel/Polygons/Cloud/CMakeLists_files.cmake b/ApplicationLibCode/ProjectDataModel/Polygons/Cloud/CMakeLists_files.cmake new file mode 100644 index 0000000000..122be7df8f --- /dev/null +++ b/ApplicationLibCode/ProjectDataModel/Polygons/Cloud/CMakeLists_files.cmake @@ -0,0 +1,8 @@ +set(SOURCE_GROUP_SOURCE_FILES + ${CMAKE_CURRENT_LIST_DIR}/RimPolygonCloudAddress.cpp + ${CMAKE_CURRENT_LIST_DIR}/RimCloudPolygon.cpp + ${CMAKE_CURRENT_LIST_DIR}/RimPolygonCloudFolder.cpp + ${CMAKE_CURRENT_LIST_DIR}/RimPolygonCloudSource.cpp +) + +list(APPEND CODE_SOURCE_FILES ${SOURCE_GROUP_SOURCE_FILES}) diff --git a/ApplicationLibCode/ProjectDataModel/Polygons/Cloud/RimCloudPolygon.cpp b/ApplicationLibCode/ProjectDataModel/Polygons/Cloud/RimCloudPolygon.cpp new file mode 100644 index 0000000000..bfdc02a4cd --- /dev/null +++ b/ApplicationLibCode/ProjectDataModel/Polygons/Cloud/RimCloudPolygon.cpp @@ -0,0 +1,108 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 "RimCloudPolygon.h" + +CAF_PDM_SOURCE_INIT( RimCloudPolygon, "RimCloudPolygon" ); + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimCloudPolygon::RimCloudPolygon() +{ + CAF_PDM_InitObject( "Sumo Polygon", ":/PolylinesFromFile16x16.png" ); + + CAF_PDM_InitFieldNoDefault( &m_caseId, "SumoCaseId", "Case Id" ); + m_caseId.uiCapability()->setUiHidden( true ); + CAF_PDM_InitFieldNoDefault( &m_ensembleName, "SumoEnsembleName", "Ensemble Name" ); + m_ensembleName.uiCapability()->setUiHidden( true ); + CAF_PDM_InitField( &m_realization, "SumoRealization", -1, "Realization" ); + m_realization.uiCapability()->setUiHidden( true ); + CAF_PDM_InitFieldNoDefault( &m_polygonResult, "SumoPolygonResult", "Polygon Result" ); + m_polygonResult.uiCapability()->setUiHidden( true ); + CAF_PDM_InitFieldNoDefault( &m_sumoName, "SumoPolygonName", "Sumo Name" ); + m_sumoName.uiCapability()->setUiHidden( true ); + CAF_PDM_InitFieldNoDefault( &m_contactType, "SumoContactType", "Fluid Contact Type" ); + m_contactType.uiCapability()->setUiHidden( true ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimCloudPolygon::setSumoIdentity( const QString& caseId, + const QString& ensembleName, + int realization, + const QString& polygonResult, + const QString& sumoName, + const QString& contactType ) +{ + m_caseId = caseId; + m_ensembleName = ensembleName; + m_realization = realization; + m_polygonResult = polygonResult; + m_sumoName = sumoName; + m_contactType = contactType; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RimCloudPolygon::caseId() const +{ + return m_caseId(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RimCloudPolygon::ensembleName() const +{ + return m_ensembleName(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +int RimCloudPolygon::realization() const +{ + return m_realization(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RimCloudPolygon::polygonResult() const +{ + return m_polygonResult(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RimCloudPolygon::sumoName() const +{ + return m_sumoName(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RimCloudPolygon::contactType() const +{ + return m_contactType(); +} diff --git a/ApplicationLibCode/ProjectDataModel/Polygons/Cloud/RimCloudPolygon.h b/ApplicationLibCode/ProjectDataModel/Polygons/Cloud/RimCloudPolygon.h new file mode 100644 index 0000000000..996a06cbab --- /dev/null +++ b/ApplicationLibCode/ProjectDataModel/Polygons/Cloud/RimCloudPolygon.h @@ -0,0 +1,64 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 "Polygons/RimPolygon.h" + +#include "cafPdmField.h" + +//================================================================================================== +/// +/// A RimPolygon fetched from Sumo, stamped with the full Sumo identity (case, ensemble, +/// realization, polygon result, name, contact type) it was fetched with -- a self-contained, +/// immutable snapshot that a RimPolygonCloudAddress can hand out for any realization without +/// external bookkeeping or mutation. +/// +/// Identity fields are hidden from the UI (traceability/debugging only) -- editing happens on +/// RimPolygonCloudAddress. +/// +//================================================================================================== +class RimCloudPolygon : public RimPolygon +{ + CAF_PDM_HEADER_INIT; + +public: + RimCloudPolygon(); + + void setSumoIdentity( const QString& caseId, + const QString& ensembleName, + int realization, + const QString& polygonResult, + const QString& sumoName, + const QString& contactType ); + + QString caseId() const; + QString ensembleName() const; + int realization() const; + QString polygonResult() const; + QString sumoName() const; + QString contactType() const; + +private: + caf::PdmField m_caseId; + caf::PdmField m_ensembleName; + caf::PdmField m_realization; + caf::PdmField m_polygonResult; + caf::PdmField m_sumoName; + caf::PdmField m_contactType; +}; diff --git a/ApplicationLibCode/ProjectDataModel/Polygons/Cloud/RimPolygonCloudAddress.cpp b/ApplicationLibCode/ProjectDataModel/Polygons/Cloud/RimPolygonCloudAddress.cpp new file mode 100644 index 0000000000..baea0d8f33 --- /dev/null +++ b/ApplicationLibCode/ProjectDataModel/Polygons/Cloud/RimPolygonCloudAddress.cpp @@ -0,0 +1,456 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 "RimPolygonCloudAddress.h" + +#include "RiaApplication.h" +#include "RiaLogging.h" + +#include "Cloud/RiaSumoConnector.h" +#include "Cloud/RimSumoDataSource.h" + +#include "Polygons/Cloud/RimCloudPolygon.h" +#include "Polygons/Cloud/RimPolygonCloudSource.h" +#include "Polygons/RimPolygon.h" +#include "Polygons/RimPolygonInViewCollection.h" + +#include "cafCmdFeatureMenuBuilder.h" +#include "cafPdmUiTreeAttributes.h" + +#include + +#include + +CAF_PDM_SOURCE_INIT( RimPolygonCloudAddress, "RimPolygonCloudAddress" ); + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimPolygonCloudAddress::RimPolygonCloudAddress() + : objectChanged( this ) +{ + CAF_PDM_InitObject( "Sumo Polygon Address", ":/CloudBlobs.svg" ); + + // Inherited fields, redeclared here so this subclass gets its own ui/xml keyword. + CAF_PDM_InitFieldNoDefault( &m_collectionName, "Name", "Name" ); + CAF_PDM_InitFieldNoDefault( &m_subCollections, "SubCollections", "Subcollections" ); + m_subCollections.uiCapability()->setUiHidden( true ); + CAF_PDM_InitFieldNoDefault( &m_items, "Polygons", "Polygons" ); + + // Fixed identity, set once via configureIdentity() -- read-only in the UI. + CAF_PDM_InitField( &m_polygonResult, "PolygonResult", QString( "field_outline" ), "Polygon Result" ); + m_polygonResult.uiCapability()->setUiReadOnly( true ); + CAF_PDM_InitField( &m_name, "PolygonName", QString(), "Name" ); + m_name.uiCapability()->setUiReadOnly( true ); + CAF_PDM_InitField( &m_contactType, "ContactType", QString(), "Fluid Contact Type" ); + m_contactType.uiCapability()->setUiReadOnly( true ); + + setDeletable( false ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimPolygonCloudAddress::~RimPolygonCloudAddress() +{ + for ( auto& [realization, polygons] : m_realizationCache ) + { + for ( auto* polygon : polygons ) + { + delete polygon; + } + } +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimPolygonCloudAddress::configureIdentity( SumoPolygonResult polygonResult, const QString& sumoName, const QString& contactType ) +{ + m_polygonResult = RiaSumoPolygons::polygonResultKey( polygonResult ); + m_name = sumoName; + m_contactType = contactType; + + updateName(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +SumoPolygonResult RimPolygonCloudAddress::polygonResult() const +{ + if ( m_polygonResult() == RiaSumoPolygons::polygonResultKey( SumoPolygonResult::StructureDepthFaultLines ) ) + { + return SumoPolygonResult::StructureDepthFaultLines; + } + if ( m_polygonResult() == RiaSumoPolygons::polygonResultKey( SumoPolygonResult::FluidContactOutline ) ) + { + return SumoPolygonResult::FluidContactOutline; + } + return SumoPolygonResult::FieldOutline; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RimPolygonCloudAddress::sumoName() const +{ + return m_name(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RimPolygonCloudAddress::contactType() const +{ + return m_contactType(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimPolygonCloudSource* RimPolygonCloudAddress::owningSource() const +{ + return firstAncestorOfType(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RimPolygonCloudAddress::hasBaseData() const +{ + return !items().empty(); +} + +//-------------------------------------------------------------------------------------------------- +/// Idempotent no-op if already fetched. +//-------------------------------------------------------------------------------------------------- +void RimPolygonCloudAddress::ensureBaseFetched() +{ + if ( hasBaseData() ) return; + + auto* source = owningSource(); + if ( !source ) return; + + auto fetchedPolygons = fetchPolygonsFromSumo( source->baseRealization() ); + + m_items.deleteChildren(); + m_items.setValue( fetchedPolygons ); + for ( auto* polygon : fetchedPolygons ) + { + ensureUniquePolygonName( polygon ); + } + + if ( fetchedPolygons.empty() ) + { + RiaLogging::warning( "No polygons found for Sumo polygon address: " + name().toStdString() ); + } + else + { + RiaLogging::info( + QString( "Fetched %1 polygon(s) from Sumo for address: %2" ).arg( fetchedPolygons.size() ).arg( name() ).toStdString() ); + } + + // Structural refresh needed: the tree node already exists with zero children. + uiCapability()->updateAllRequiredEditors(); + + objectChanged.send(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimPolygonCloudAddress::evictBaseData() +{ + if ( !hasBaseData() ) return; + + m_items.deleteChildren(); + uiCapability()->updateAllRequiredEditors(); + objectChanged.send(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RimPolygonCloudAddress::hasDataForRealization( int realization ) const +{ + auto it = m_realizationCache.find( realization ); + return it != m_realizationCache.end() && !it->second.empty(); +} + +//-------------------------------------------------------------------------------------------------- +/// Idempotent no-op if already cached. +//-------------------------------------------------------------------------------------------------- +void RimPolygonCloudAddress::ensureRealizationFetched( int realization ) +{ + if ( m_realizationCache.find( realization ) != m_realizationCache.end() ) return; + + auto fetchedPolygons = fetchPolygonsFromSumo( realization ); + for ( auto* polygon : fetchedPolygons ) + { + ensureUniquePolygonName( polygon ); + } + + m_realizationCache[realization] = fetchedPolygons; + + if ( fetchedPolygons.empty() ) + { + RiaLogging::warning( + QString( "No polygons found for Sumo polygon address: %1 (realization %2)" ).arg( name() ).arg( realization ).toStdString() ); + } + else + { + RiaLogging::info( QString( "Fetched %1 polygon(s) from Sumo for address: %2 (realization %3)" ) + .arg( fetchedPolygons.size() ) + .arg( name() ) + .arg( realization ) + .toStdString() ); + } +} + +//-------------------------------------------------------------------------------------------------- +/// Pure read, never fetches. Empty if not cached, see ensureRealizationFetched(). +//-------------------------------------------------------------------------------------------------- +std::vector RimPolygonCloudAddress::cachedItemsForRealization( int realization ) const +{ + auto it = m_realizationCache.find( realization ); + if ( it == m_realizationCache.end() ) return {}; + return it->second; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimPolygonCloudAddress::evictRealizationIfUnused( int realization ) +{ + auto it = m_realizationCache.find( realization ); + if ( it == m_realizationCache.end() ) return; + + for ( auto* polygon : it->second ) + { + delete polygon; + } + m_realizationCache.erase( it ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector RimPolygonCloudAddress::cachedRealizations() const +{ + std::vector realizations; + for ( const auto& [realization, polygons] : m_realizationCache ) + { + realizations.push_back( realization ); + } + return realizations; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RimPolygonCloudAddress::name() const +{ + QString nameCandidate = m_collectionName.value(); + if ( !nameCandidate.isEmpty() ) return nameCandidate; + + return "Sumo Polygon Address"; +} + +//-------------------------------------------------------------------------------------------------- +/// Realization groups are only ever added programmatically. +//-------------------------------------------------------------------------------------------------- +bool RimPolygonCloudAddress::canAddSubCollection() const +{ + return false; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimPolygonContainer* RimPolygonCloudAddress::addNewSubCollection() +{ + return nullptr; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimPolygonCloudAddress::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) +{ + uiOrdering.add( &m_collectionName ); + uiOrdering.add( &m_polygonResult ); + + const bool isFieldOutline = ( m_polygonResult() == RiaSumoPolygons::polygonResultKey( SumoPolygonResult::FieldOutline ) ); + const bool isFluidContact = ( m_polygonResult() == RiaSumoPolygons::polygonResultKey( SumoPolygonResult::FluidContactOutline ) ); + + m_name.uiCapability()->setUiHidden( isFieldOutline ); + uiOrdering.add( &m_name ); + + m_contactType.uiCapability()->setUiHidden( !isFluidContact ); + uiOrdering.add( &m_contactType ); + + uiOrdering.skipRemainingFields(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimPolygonCloudAddress::appendMenuItems( caf::CmdFeatureMenuBuilder& menuBuilder ) const +{ + menuBuilder << "RicReloadPolygonCloudAddressFeature"; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimPolygonCloudAddress::defineObjectEditorAttribute( QString uiConfigName, caf::PdmUiEditorAttribute* attribute ) +{ + if ( hasBaseData() ) return; + + auto* treeItemAttribute = dynamic_cast( attribute ); + if ( !treeItemAttribute ) return; + + // Clickable download icon -- fetches base data directly from the tree. + auto downloadTag = caf::PdmUiTreeViewItemAttribute::createTag(); + downloadTag->icon = caf::IconProvider( ":/Download.svg" ); + downloadTag->toolTip = "Fetch polygon data"; + downloadTag->clicked.connect( this, &RimPolygonCloudAddress::onDownloadTagClicked ); + treeItemAttribute->tags.push_back( std::move( downloadTag ) ); + + // Plain "not fetched" marker, no colored pill. + auto textTag = caf::PdmUiTreeViewItemAttribute::createTag(); + textTag->text = "[Not fetched]"; + textTag->bgColor = QColor( Qt::white ); + textTag->fgColor = QColor( Qt::darkGray ); + treeItemAttribute->tags.push_back( std::move( textTag ) ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimPolygonCloudAddress::onDownloadTagClicked( const caf::SignalEmitter* emitter, size_t index ) +{ + ensureBaseFetched(); + + updateAllRequiredEditors(); + objectChanged.send(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RiaSumoConnector* RimPolygonCloudAddress::sumoConnector() +{ + return RiaApplication::instance()->makeSumoConnector(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimPolygonCloudAddress::updateName() +{ + QString name; + + if ( m_polygonResult() == RiaSumoPolygons::polygonResultKey( SumoPolygonResult::FieldOutline ) ) + { + // Only one, unnamed field outline leaf -- use the category label itself. + name = polygonResultLabel( SumoPolygonResult::FieldOutline ); + } + else if ( m_polygonResult() == RiaSumoPolygons::polygonResultKey( SumoPolygonResult::StructureDepthFaultLines ) ) + { + // Owning folder already conveys the category. + name = m_name(); + } + else if ( m_polygonResult() == RiaSumoPolygons::polygonResultKey( SumoPolygonResult::FluidContactOutline ) ) + { + // Owning folder chain already conveys category and name -- show contact type only. + name = m_contactType(); + } + + setCollectionName( name ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RimPolygonCloudAddress::polygonResultLabel( SumoPolygonResult polygonResult ) +{ + switch ( polygonResult ) + { + case SumoPolygonResult::FieldOutline: + return "Field Outline"; + case SumoPolygonResult::StructureDepthFaultLines: + return "Structure Depth Fault Lines"; + case SumoPolygonResult::FluidContactOutline: + return "Fluid Contact Outline"; + } + + return {}; +} + +//-------------------------------------------------------------------------------------------------- +/// Fetches from Sumo for the given realization, using this address's own fixed identity. +//-------------------------------------------------------------------------------------------------- +std::vector RimPolygonCloudAddress::fetchPolygonsFromSumo( int realization ) +{ + std::vector polygons; + + auto* source = owningSource(); + if ( !source ) return polygons; + + auto* dataSource = source->dataSource(); + auto* connector = sumoConnector(); + if ( !dataSource || !connector ) return polygons; + + auto polygonDataList = connector->polygons().polygonsData( dataSource->caseId(), + dataSource->ensembleName(), + realization, + polygonResult(), + m_name(), + m_contactType() ); + + const bool nameGroupsAreDistinct = polygonDataList.size() > 1; + + for ( const auto& data : polygonDataList ) + { + auto* polygon = new RimCloudPolygon(); + polygon->disableStorageOfPolygonPoints(); + polygon->setReadOnly( true ); + polygon->setDeletable( false ); + polygon->setSumoIdentity( dataSource->caseId().get(), dataSource->ensembleName(), realization, m_polygonResult(), m_name(), m_contactType() ); + + QString polygonName = data.name.isEmpty() ? name() : data.name; + if ( nameGroupsAreDistinct ) polygonName = QString( "%1 (ID: %2)" ).arg( polygonName ).arg( data.polyId ); + polygon->setName( polygonName ); + + std::vector points; + const size_t pointCount = std::min( { data.xArr.size(), data.yArr.size(), data.zArr.size() } ); + points.reserve( pointCount ); + for ( size_t i = 0; i < pointCount; i++ ) + { + // Sumo's depth is positive-down; ResInsight's domain z is elevation (negative-down). + points.emplace_back( data.xArr[i], data.yArr[i], -data.zArr[i] ); + } + polygon->setPointsInDomainCoords( points ); + + polygons.push_back( polygon ); + } + + return polygons; +} diff --git a/ApplicationLibCode/ProjectDataModel/Polygons/Cloud/RimPolygonCloudAddress.h b/ApplicationLibCode/ProjectDataModel/Polygons/Cloud/RimPolygonCloudAddress.h new file mode 100644 index 0000000000..953d765958 --- /dev/null +++ b/ApplicationLibCode/ProjectDataModel/Polygons/Cloud/RimPolygonCloudAddress.h @@ -0,0 +1,114 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 "Polygons/RimPolygonContainer.h" + +#include "Cloud/RiaSumoPolygons.h" + +#include "cafPdmField.h" +#include "cafSignal.h" + +#include +#include + +class RimPolygon; +class RimCloudPolygon; +class RimPolygonCloudSource; +class RiaSumoConnector; + +//================================================================================================== +/// +/// Leaf polygon container for one unique polygon table in Sumo (polygon result category, name, +/// and for fluid contacts a contact type). Identity is fixed once by +/// RimPolygonCloudSource::buildDirectoryTree() and never edited afterward. +/// +/// Coordinate data (RimCloudPolygon children) is not persisted and is fetched lazily, the first +/// time a view's visibility checkbox for this leaf is checked (see +/// RimPolygonInViewCollection::prepareForSync()), and evicted once no view shows it checked. +/// +/// An additional, unparented cache holds data for other realizations a view is "Auto-Follow"-ing. +/// These are never PDM children, so the tree always shows only the base realization. +/// +//================================================================================================== +class RimPolygonCloudAddress : public RimPolygonContainer +{ + CAF_PDM_HEADER_INIT; + +public: + caf::Signal<> objectChanged; + +public: + RimPolygonCloudAddress(); + ~RimPolygonCloudAddress() override; + + // Fixed identity, set once by RimPolygonCloudSource::buildDirectoryTree(). + void configureIdentity( SumoPolygonResult polygonResult, const QString& sumoName, const QString& contactType ); + + SumoPolygonResult polygonResult() const; + QString sumoName() const; + QString contactType() const; + + RimPolygonCloudSource* owningSource() const; + + // Base-realization data: items() for the owning source's base realization. Lazily fetched, + // idempotent. + bool hasBaseData() const; + void ensureBaseFetched(); + void evictBaseData(); + + // Cache for other realizations (Auto-Follow). Never added as PDM children. + bool hasDataForRealization( int realization ) const; + void ensureRealizationFetched( int realization ); + std::vector cachedItemsForRealization( int realization ) const; + void evictRealizationIfUnused( int realization ); + std::vector cachedRealizations() const; + + QString name() const; + + bool canAddSubCollection() const override; + RimPolygonContainer* addNewSubCollection() override; + +protected: + void defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) override; + void appendMenuItems( caf::CmdFeatureMenuBuilder& menuBuilder ) const override; + void defineObjectEditorAttribute( QString uiConfigName, caf::PdmUiEditorAttribute* attribute ) override; + +private: + RiaSumoConnector* sumoConnector(); + void updateName(); + + // Tree download-tag click handler: fetches base data directly, independent of view checkboxes. + void onDownloadTagClicked( const caf::SignalEmitter* emitter, size_t index ); + + static QString polygonResultLabel( SumoPolygonResult polygonResult ); + + // Fetches from Sumo for the given realization, stamping each RimCloudPolygon with its Sumo + // identity. Used for both base and comparison realizations. + std::vector fetchPolygonsFromSumo( int realization ); + +private: + caf::PdmField m_polygonResult; + caf::PdmField m_name; + caf::PdmField m_contactType; + + // Cache of RimCloudPolygon objects for non-base realizations. Owned/deleted here, never PDM + // children. + std::map> m_realizationCache; +}; diff --git a/ApplicationLibCode/ProjectDataModel/Polygons/Cloud/RimPolygonCloudFolder.cpp b/ApplicationLibCode/ProjectDataModel/Polygons/Cloud/RimPolygonCloudFolder.cpp new file mode 100644 index 0000000000..5c55e8c770 --- /dev/null +++ b/ApplicationLibCode/ProjectDataModel/Polygons/Cloud/RimPolygonCloudFolder.cpp @@ -0,0 +1,64 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 "RimPolygonCloudFolder.h" + +#include "Polygons/RimPolygon.h" + +CAF_PDM_SOURCE_INIT( RimPolygonCloudFolder, "RimPolygonCloudFolder" ); + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimPolygonCloudFolder::RimPolygonCloudFolder() +{ + CAF_PDM_InitObject( "Folder", ":/Folder.png" ); + + CAF_PDM_InitFieldNoDefault( &m_collectionName, "Name", "Name" ); + CAF_PDM_InitFieldNoDefault( &m_subCollections, "SubCollections", "Subcollections" ); + m_subCollections.uiCapability()->setUiHidden( true ); + CAF_PDM_InitFieldNoDefault( &m_items, "Polygons", "Polygons" ); + m_items.uiCapability()->setUiHidden( true ); + + setDeletable( false ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimPolygonCloudFolder::RimPolygonCloudFolder( const QString& folderName ) + : RimPolygonCloudFolder() +{ + setCollectionName( folderName ); +} + +//-------------------------------------------------------------------------------------------------- +/// Only built programmatically by RimPolygonCloudSource::buildDirectoryTree(). +//-------------------------------------------------------------------------------------------------- +bool RimPolygonCloudFolder::canAddSubCollection() const +{ + return false; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimPolygonContainer* RimPolygonCloudFolder::addNewSubCollection() +{ + return nullptr; +} diff --git a/ApplicationLibCode/ProjectDataModel/Polygons/Cloud/RimPolygonCloudFolder.h b/ApplicationLibCode/ProjectDataModel/Polygons/Cloud/RimPolygonCloudFolder.h new file mode 100644 index 0000000000..c05c71d409 --- /dev/null +++ b/ApplicationLibCode/ProjectDataModel/Polygons/Cloud/RimPolygonCloudFolder.h @@ -0,0 +1,40 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 "Polygons/RimPolygonContainer.h" + +//================================================================================================== +/// +/// Plain, non-user-editable organizational folder for the tree beneath a RimPolygonCloudSource +/// (e.g. "Field Outline", "Fluid Contact Outline" + per-name folders). Only +/// RimPolygonCloudSource::buildDirectoryTree() creates/populates these. +/// +//================================================================================================== +class RimPolygonCloudFolder : public RimPolygonContainer +{ + CAF_PDM_HEADER_INIT; + +public: + RimPolygonCloudFolder(); + explicit RimPolygonCloudFolder( const QString& folderName ); + + bool canAddSubCollection() const override; + RimPolygonContainer* addNewSubCollection() override; +}; diff --git a/ApplicationLibCode/ProjectDataModel/Polygons/Cloud/RimPolygonCloudSource.cpp b/ApplicationLibCode/ProjectDataModel/Polygons/Cloud/RimPolygonCloudSource.cpp new file mode 100644 index 0000000000..7129b47ac6 --- /dev/null +++ b/ApplicationLibCode/ProjectDataModel/Polygons/Cloud/RimPolygonCloudSource.cpp @@ -0,0 +1,447 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 "RimPolygonCloudSource.h" + +#include "RiaApplication.h" + +#include "Cloud/RiaSumoConnector.h" +#include "Cloud/RimCloudDataSourceCollection.h" +#include "Cloud/RimSumoDataSource.h" + +#include "Polygons/Cloud/RimPolygonCloudAddress.h" +#include "Polygons/Cloud/RimPolygonCloudFolder.h" +#include "Polygons/RimPolygon.h" + +#include "Polygons/RimPolygonInViewCollection.h" + +#include "Rim3dView.h" +#include "RimRoffCaseSumo.h" + +#include "cafCmdFeatureMenuBuilder.h" +#include "cafPdmUiButtonBox.h" +#include "cafPdmUiComboBoxEditor.h" + +#include + +CAF_PDM_SOURCE_INIT( RimPolygonCloudSource, "RimPolygonCloudSource" ); + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimPolygonCloudSource::RimPolygonCloudSource() + : objectChanged( this ) +{ + CAF_PDM_InitObject( "Cloud Polygon Source", ":/CloudBlobs.svg" ); + + CAF_PDM_InitFieldNoDefault( &m_collectionName, "Name", "Name" ); + CAF_PDM_InitFieldNoDefault( &m_subCollections, "SubCollections", "Subcollections" ); + m_subCollections.uiCapability()->setUiHidden( true ); + CAF_PDM_InitFieldNoDefault( &m_items, "Polygons", "Polygons" ); + m_items.uiCapability()->setUiHidden( true ); + + CAF_PDM_InitFieldNoDefault( &m_dataSource, "DataSource", "Data Source" ); + m_dataSource.uiCapability()->setUiEditorTypeName( caf::PdmUiComboBoxEditor::uiEditorTypeName() ); + CAF_PDM_InitField( &m_baseRealization, "BaseRealization", -1, "Base Realization" ); + m_baseRealization.uiCapability()->setUiEditorTypeName( caf::PdmUiComboBoxEditor::uiEditorTypeName() ); + + CAF_PDM_InitFieldNoDefault( &m_appliedDataSource, "AppliedDataSource", "Applied Data Source" ); + m_appliedDataSource.uiCapability()->setUiHidden( true ); + CAF_PDM_InitField( &m_appliedBaseRealization, "AppliedBaseRealization", -1, "Applied Base Realization" ); + m_appliedBaseRealization.uiCapability()->setUiHidden( true ); + + setDeletable( true ); +} + +//-------------------------------------------------------------------------------------------------- +/// One-shot setup (e.g. from a creation command); sets pending and applied directly, no fetch. +//-------------------------------------------------------------------------------------------------- +void RimPolygonCloudSource::setDataSource( RimSumoDataSource* dataSource ) +{ + m_dataSource = dataSource; + m_appliedDataSource = dataSource; + updateName(); +} + +//-------------------------------------------------------------------------------------------------- +/// See setDataSource(). +//-------------------------------------------------------------------------------------------------- +void RimPolygonCloudSource::setBaseRealization( int realization ) +{ + m_baseRealization = realization; + m_appliedBaseRealization = realization; + updateName(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimSumoDataSource* RimPolygonCloudSource::dataSource() const +{ + return m_appliedDataSource(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +int RimPolygonCloudSource::baseRealization() const +{ + return m_appliedBaseRealization(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RimPolygonCloudSource::isDirectoryBuilt() const +{ + return m_directoryBuilt; +} + +//-------------------------------------------------------------------------------------------------- +/// Fetches the directory metadata once and builds the folder/leaf tree for the applied data +/// source/base realization. No-op if already built. +//-------------------------------------------------------------------------------------------------- +void RimPolygonCloudSource::buildDirectoryTree() +{ + if ( m_directoryBuilt ) return; + + auto* dataSource = m_appliedDataSource(); + auto* connector = sumoConnector(); + if ( !dataSource || m_appliedBaseRealization() < 0 || !connector ) return; + + auto directory = connector->polygons().polygonResultDirectory( dataSource->caseId(), dataSource->ensembleName() ); + + if ( !directory.fieldOutline.empty() ) + { + auto* folder = new RimPolygonCloudFolder( "Field Outline" ); + auto* address = new RimPolygonCloudAddress(); + address->configureIdentity( SumoPolygonResult::FieldOutline, QString(), QString() ); + folder->addSubCollection( address ); + addSubCollection( folder ); + } + + if ( !directory.structureDepthFaultLines.empty() ) + { + auto* folder = new RimPolygonCloudFolder( "Structure Depth Fault Lines" ); + + std::vector namesAdded; + for ( const auto& meta : directory.structureDepthFaultLines ) + { + if ( std::ranges::find( namesAdded, meta.name ) != namesAdded.end() ) continue; + namesAdded.push_back( meta.name ); + + auto* address = new RimPolygonCloudAddress(); + address->configureIdentity( SumoPolygonResult::StructureDepthFaultLines, meta.name, QString() ); + folder->addSubCollection( address ); + } + + addSubCollection( folder ); + } + + if ( !directory.fluidContactOutline.empty() ) + { + auto* resultFolder = new RimPolygonCloudFolder( "Fluid Contact Outline" ); + + std::vector namesAdded; + for ( const auto& meta : directory.fluidContactOutline ) + { + if ( std::ranges::find( namesAdded, meta.name ) != namesAdded.end() ) continue; + namesAdded.push_back( meta.name ); + + auto* nameFolder = new RimPolygonCloudFolder( meta.name ); + + for ( const auto& contactMeta : directory.fluidContactOutline ) + { + if ( contactMeta.name != meta.name || contactMeta.contactType.isEmpty() ) continue; + + auto* address = new RimPolygonCloudAddress(); + address->configureIdentity( SumoPolygonResult::FluidContactOutline, meta.name, contactMeta.contactType ); + nameFolder->addSubCollection( address ); + } + + resultFolder->addSubCollection( nameFolder ); + } + + addSubCollection( resultFolder ); + } + + m_directoryBuilt = true; + + updateConnectedEditors(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RimPolygonCloudSource::name() const +{ + return collectionName(); +} + +//-------------------------------------------------------------------------------------------------- +/// Tree is only ever built by buildDirectoryTree(). +//-------------------------------------------------------------------------------------------------- +bool RimPolygonCloudSource::canAddSubCollection() const +{ + return false; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimPolygonContainer* RimPolygonCloudSource::addNewSubCollection() +{ + return nullptr; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RimPolygonCloudSource::supportsRealizationOverride() const +{ + return true; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector RimPolygonCloudSource::availableRealizationIdsForOverride() const +{ + std::vector realizations; + + if ( auto* ds = m_dataSource() ) + { + for ( const auto& realizationId : ds->selectedRealizationIds() ) + { + bool ok = false; + int value = realizationId.toInt( &ok ); + if ( ok ) realizations.push_back( value ); + } + } + + return realizations; +} + +//-------------------------------------------------------------------------------------------------- +/// Follows a view's realization only when its case is a RimRoffCaseSumo from this same data +/// source -- never a case belonging to a different field/ensemble. +//-------------------------------------------------------------------------------------------------- +int RimPolygonCloudSource::resolveViewMatchingRealization( const Rim3dView* view ) const +{ + if ( !view || !m_appliedDataSource() ) return -1; + + if ( auto* sumoCase = dynamic_cast( view->ownerCase() ) ) + { + if ( sumoCase->dataSource() == m_appliedDataSource() ) + { + return sumoCase->realization(); + } + } + + return -1; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimPolygonCloudSource::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) +{ + uiOrdering.add( &m_collectionName ); + uiOrdering.add( &m_dataSource ); + uiOrdering.add( &m_baseRealization ); + + const bool hasValidPendingSelection = m_dataSource() != nullptr && m_baseRealization() >= 0; + const bool pending = hasPendingChanges(); + + auto* buttonBox = uiOrdering.addNewButtonBox(); + + auto& applyButton = buttonBox->addButton( "Apply", [this]() { onApplyClicked(); } ); + applyButton.enabled = hasValidPendingSelection && pending; + applyButton.toolTip = hasValidPendingSelection ? QString( "" ) : QString( "Select a Data Source and Base Realization first." ); + + auto& cancelButton = buttonBox->addButton( "Cancel", [this]() { onCancelClicked(); } ); + cancelButton.enabled = m_directoryBuilt && pending; + cancelButton.toolTip = "Discards the pending edits above, restoring the currently applied Data Source and Base Realization."; + + uiOrdering.skipRemainingFields(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimPolygonCloudSource::appendMenuItems( caf::CmdFeatureMenuBuilder& menuBuilder ) const +{ +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimPolygonCloudSource::fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue ) +{ + if ( changedField == &m_dataSource ) + { + // New data source invalidates the previously picked realization. + auto realizations = availableRealizationIdsForOverride(); + m_baseRealization = realizations.empty() ? -1 : realizations.front(); + } + + updateConnectedEditors(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QList RimPolygonCloudSource::calculateValueOptions( const caf::PdmFieldHandle* fieldNeedingOptions ) +{ + QList options; + + if ( fieldNeedingOptions == &m_dataSource ) + { + for ( auto* ds : RimCloudDataSourceCollection::instance()->sumoDataSources() ) + { + options.push_back( caf::PdmOptionItemInfo( ds->name(), ds ) ); + } + } + else if ( fieldNeedingOptions == &m_baseRealization ) + { + for ( int realization : availableRealizationIdsForOverride() ) + { + options.push_back( caf::PdmOptionItemInfo( QString::number( realization ), realization ) ); + } + } + + return options; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RiaSumoConnector* RimPolygonCloudSource::sumoConnector() +{ + return RiaApplication::instance()->makeSumoConnector(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimPolygonCloudSource::updateName() +{ + setCollectionName( composeName( m_appliedBaseRealization() ) ); +} + +//-------------------------------------------------------------------------------------------------- +/// Shared by updateName() (applied base realization) and nameForRealization() (a view mirror +/// Auto-Following a different realization). +//-------------------------------------------------------------------------------------------------- +QString RimPolygonCloudSource::composeName( int realization ) const +{ + if ( !m_appliedDataSource() ) return "Cloud Polygon Source"; + + if ( realization < 0 ) return m_appliedDataSource()->name(); + + return QString( "%1 / Real %2" ).arg( m_appliedDataSource()->name() ).arg( realization ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RimPolygonCloudSource::nameForRealization( int realization ) const +{ + return composeName( realization ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RimPolygonCloudSource::hasPendingChanges() const +{ + if ( !m_directoryBuilt ) return true; + + return m_dataSource() != m_appliedDataSource() || m_baseRealization() != m_appliedBaseRealization(); +} + +//-------------------------------------------------------------------------------------------------- +/// Commits the pending selection. First Apply builds the tree; a later Apply rebuilds it if the +/// data source changed, or just evicts stale base data if only the realization changed. +//-------------------------------------------------------------------------------------------------- +void RimPolygonCloudSource::onApplyClicked() +{ + const bool wasBuilt = m_directoryBuilt; + const bool dataSourceChanged = m_dataSource() != m_appliedDataSource(); + + m_appliedDataSource = m_dataSource(); + m_appliedBaseRealization = m_baseRealization(); + + updateName(); + + if ( wasBuilt && dataSourceChanged ) + { + // Ensemble identity changed -- the existing tree no longer applies. + m_subCollections.deleteChildren(); + m_directoryBuilt = false; + } + else if ( wasBuilt ) + { + // Only the base realization changed -- structure stays, evict stale fetched data. + for ( auto* address : allAddresses() ) + { + address->evictBaseData(); + } + } + + buildDirectoryTree(); + + updateConnectedEditors(); + uiCapability()->updateAllRequiredEditors(); + + objectChanged.send(); +} + +//-------------------------------------------------------------------------------------------------- +/// Discards pending edits, restoring pending fields to the applied selection. No fetch. +//-------------------------------------------------------------------------------------------------- +void RimPolygonCloudSource::onCancelClicked() +{ + m_dataSource = m_appliedDataSource(); + m_baseRealization = m_appliedBaseRealization(); + + updateConnectedEditors(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector RimPolygonCloudSource::allAddresses() const +{ + std::vector addresses; + + std::function visit = [&]( const RimPolygonContainer* container ) + { + for ( auto* sub : container->subCollections() ) + { + if ( auto* address = dynamic_cast( sub ) ) + { + addresses.push_back( address ); + } + visit( sub ); + } + }; + visit( this ); + + return addresses; +} diff --git a/ApplicationLibCode/ProjectDataModel/Polygons/Cloud/RimPolygonCloudSource.h b/ApplicationLibCode/ProjectDataModel/Polygons/Cloud/RimPolygonCloudSource.h new file mode 100644 index 0000000000..4e1223ecc8 --- /dev/null +++ b/ApplicationLibCode/ProjectDataModel/Polygons/Cloud/RimPolygonCloudSource.h @@ -0,0 +1,111 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// 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 "Polygons/RimPolygonContainer.h" + +#include "cafPdmField.h" +#include "cafPdmPtrField.h" +#include "cafSignal.h" + +class RimSumoDataSource; +class RimPolygonCloudAddress; +class RiaSumoConnector; +class Rim3dView; + +//================================================================================================== +/// +/// Root of a browsable tree of Sumo polygon results for one (case, ensemble, base realization). +/// Created via "Add Cloud Polygon Source"; the user picks a Data Source and Base Realization and +/// clicks "Apply". The first Apply does one cheap directory metadata fetch and builds the entire +/// folder/leaf structure (Field Outline / Structure Depth Fault Lines + names / Fluid Contact +/// Outline + names + contact types) as RimPolygonCloudFolder/RimPolygonCloudAddress children -- no +/// coordinate data is fetched yet. Each leaf fetches its own coordinate data lazily, the first +/// time a view's visibility checkbox for it is checked on (see RimPolygonInViewCollection). +/// +/// Data Source/Base Realization use the same pending/applied Apply-Cancel pattern +/// RimPolygonCloudAddress used to have. Re-applying after the tree is built: if the data source +/// changed, the whole tree is torn down and rebuilt; if only the base realization changed, the +/// tree structure is kept and only already-fetched base data is evicted for lazy re-fetch. +/// +/// This is also the only container type offering a per-view "Auto-Follow View Realization" choice +/// (supportsRealizationOverride()), shown once per source and governing every leaf beneath it. +/// +//================================================================================================== +class RimPolygonCloudSource : public RimPolygonContainer +{ + CAF_PDM_HEADER_INIT; + +public: + RimPolygonCloudSource(); + + // Emitted whenever Apply changes what should be shown in an open view's mirror. Connected by + // RimPolygonCollection to trigger view resync + redraw. + caf::Signal<> objectChanged; + + void setDataSource( RimSumoDataSource* dataSource ); + void setBaseRealization( int realization ); + + RimSumoDataSource* dataSource() const; + int baseRealization() const; + + bool isDirectoryBuilt() const; + + // Fetches the directory metadata once and builds the folder/leaf tree. No-op if already built + // or no data source selected. + void buildDirectoryTree(); + + QString name() const; + + // Composes this source's display name for an arbitrary realization, e.g. "iter-0 (case) / + // Real " -- used by RimPolygonInViewCollection for a view Auto-Following a realization + // other than this source's own applied base one. + QString nameForRealization( int realization ) const; + + bool canAddSubCollection() const override; + RimPolygonContainer* addNewSubCollection() override; + + bool supportsRealizationOverride() const override; + std::vector availableRealizationIdsForOverride() const override; + int resolveViewMatchingRealization( const Rim3dView* view ) const override; + +protected: + void defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) override; + void appendMenuItems( caf::CmdFeatureMenuBuilder& menuBuilder ) const override; + void fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue ) override; + QList calculateValueOptions( const caf::PdmFieldHandle* fieldNeedingOptions ) override; + +private: + RiaSumoConnector* sumoConnector(); + void updateName(); + QString composeName( int realization ) const; + std::vector allAddresses() const; + bool hasPendingChanges() const; + void onApplyClicked(); + void onCancelClicked(); + +private: + caf::PdmPtrField m_dataSource; + caf::PdmField m_baseRealization; + + caf::PdmPtrField m_appliedDataSource; + caf::PdmField m_appliedBaseRealization; + + bool m_directoryBuilt = false; +}; diff --git a/ApplicationLibCode/ProjectDataModel/Polygons/RimPolygonCollection.cpp b/ApplicationLibCode/ProjectDataModel/Polygons/RimPolygonCollection.cpp index a4b6c591ae..a990d7a82a 100644 --- a/ApplicationLibCode/ProjectDataModel/Polygons/RimPolygonCollection.cpp +++ b/ApplicationLibCode/ProjectDataModel/Polygons/RimPolygonCollection.cpp @@ -29,6 +29,9 @@ #include "RimPolygonInViewCollection.h" #include "RimProject.h" +#include "Polygons/Cloud/RimPolygonCloudAddress.h" +#include "Polygons/Cloud/RimPolygonCloudSource.h" + #include "cafPdmFieldScriptingCapability.h" #include "cafPdmObjectScriptingCapability.h" @@ -154,6 +157,37 @@ void RimPolygonCollection::addPolygonFile( RimPolygonFile* polygonFile ) scheduleRedrawViews(); } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimPolygonCollection::addPolygonCloudAddress( RimPolygonCloudAddress* polygonCloudAddress ) +{ + if ( !polygonCloudAddress ) return; + + addSubCollection( polygonCloudAddress ); + RiaNameUniquenessTools::ensureUniqueAmongSiblings( polygonCloudAddress ); + connectPolygonCloudAddressSignals( polygonCloudAddress ); + + updateViewTreeItems(); + scheduleRedrawViews(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimPolygonCollection::addPolygonCloudSource( RimPolygonCloudSource* polygonCloudSource ) +{ + if ( !polygonCloudSource ) return; + + addSubCollection( polygonCloudSource ); + RiaNameUniquenessTools::ensureUniqueAmongSiblings( polygonCloudSource ); + connectSignalsForContainer( polygonCloudSource ); + connectPolygonCloudSourceSignals( polygonCloudSource ); + + updateViewTreeItems(); + scheduleRedrawViews(); +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -161,6 +195,7 @@ void RimPolygonCollection::appendPolygonMenuItems( caf::CmdFeatureMenuBuilder& m { menuBuilder << "RicCreatePolygonFeature"; menuBuilder << "RicImportPolygonFileFeature"; + menuBuilder << "RicAddCloudPolygonSourceFeature"; } //-------------------------------------------------------------------------------------------------- @@ -295,6 +330,28 @@ void RimPolygonCollection::connectPolygonFileSignals( RimPolygonFile* polygonFil } } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimPolygonCollection::connectPolygonCloudAddressSignals( RimPolygonCloudAddress* polygonCloudAddress ) +{ + if ( polygonCloudAddress ) + { + polygonCloudAddress->objectChanged.connect( this, &RimPolygonCollection::onPolygonCloudAddressChanged ); + } +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimPolygonCollection::connectPolygonCloudSourceSignals( RimPolygonCloudSource* polygonCloudSource ) +{ + if ( polygonCloudSource ) + { + polygonCloudSource->objectChanged.connect( this, &RimPolygonCollection::onPolygonCloudSourceChanged ); + } +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -312,6 +369,24 @@ void RimPolygonCollection::onPolygonFileChanged( const caf::SignalEmitter* emitt scheduleRedrawViews(); } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimPolygonCollection::onPolygonCloudAddressChanged( const caf::SignalEmitter* emitter ) +{ + updateViewTreeItems(); + scheduleRedrawViews(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimPolygonCollection::onPolygonCloudSourceChanged( const caf::SignalEmitter* emitter ) +{ + updateViewTreeItems(); + scheduleRedrawViews(); +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -339,6 +414,14 @@ void RimPolygonCollection::connectSignalsForContainer( RimPolygonContainer* cont { connectPolygonFileSignals( file ); } + else if ( auto* cloudAddress = dynamic_cast( sub ) ) + { + connectPolygonCloudAddressSignals( cloudAddress ); + } + else if ( auto* cloudSource = dynamic_cast( sub ) ) + { + connectPolygonCloudSourceSignals( cloudSource ); + } connectSignalsForContainer( sub ); } } diff --git a/ApplicationLibCode/ProjectDataModel/Polygons/RimPolygonCollection.h b/ApplicationLibCode/ProjectDataModel/Polygons/RimPolygonCollection.h index 5801c4f093..3d2495842e 100644 --- a/ApplicationLibCode/ProjectDataModel/Polygons/RimPolygonCollection.h +++ b/ApplicationLibCode/ProjectDataModel/Polygons/RimPolygonCollection.h @@ -22,6 +22,8 @@ class RimPolygon; class RimPolygonFile; +class RimPolygonCloudAddress; +class RimPolygonCloudSource; //================================================================================================== /// @@ -53,6 +55,13 @@ public: // be triggered another way (e.g., during initAfterRead). void addPolygonFile( RimPolygonFile* polygonFile ); + // Same as addPolygonFile, for a cloud-backed polygon address. + void addPolygonCloudAddress( RimPolygonCloudAddress* polygonCloudAddress ); + + // Adds a Sumo cloud polygon source (a whole browsable tree) as a sub-collection and wires the + // same runtime side effects. + void addPolygonCloudSource( RimPolygonCloudSource* polygonCloudSource ); + static void appendPolygonMenuItems( caf::CmdFeatureMenuBuilder& menuBuilder ); // Update editors and the polygon-in-view collections of all grid views, so that added or removed @@ -77,6 +86,12 @@ private: void onPolygonChanged( const caf::SignalEmitter* emitter ); void onPolygonFileChanged( const caf::SignalEmitter* emitter ); + void connectPolygonCloudAddressSignals( RimPolygonCloudAddress* polygonCloudAddress ); + void onPolygonCloudAddressChanged( const caf::SignalEmitter* emitter ); + + void connectPolygonCloudSourceSignals( RimPolygonCloudSource* polygonCloudSource ); + void onPolygonCloudSourceChanged( const caf::SignalEmitter* emitter ); + void connectSignalsRecursively(); void connectSignalsForContainer( RimPolygonContainer* container ); diff --git a/ApplicationLibCode/ProjectDataModel/Polygons/RimPolygonContainer.cpp b/ApplicationLibCode/ProjectDataModel/Polygons/RimPolygonContainer.cpp index ea58ec6eb2..c8e8f0c332 100644 --- a/ApplicationLibCode/ProjectDataModel/Polygons/RimPolygonContainer.cpp +++ b/ApplicationLibCode/ProjectDataModel/Polygons/RimPolygonContainer.cpp @@ -20,6 +20,7 @@ #include "RiaNameUniquenessTools.h" +#include "Rim3dView.h" #include "RimPolygon.h" #include "RimPolygonCollection.h" @@ -71,6 +72,30 @@ void RimPolygonContainer::loadData() } } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RimPolygonContainer::supportsRealizationOverride() const +{ + return false; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector RimPolygonContainer::availableRealizationIdsForOverride() const +{ + return {}; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +int RimPolygonContainer::resolveViewMatchingRealization( const Rim3dView* view ) const +{ + return -1; +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/ProjectDataModel/Polygons/RimPolygonContainer.h b/ApplicationLibCode/ProjectDataModel/Polygons/RimPolygonContainer.h index e7cc16f3de..e7ccec9720 100644 --- a/ApplicationLibCode/ProjectDataModel/Polygons/RimPolygonContainer.h +++ b/ApplicationLibCode/ProjectDataModel/Polygons/RimPolygonContainer.h @@ -21,6 +21,7 @@ #include "cafPdmNestedCollection.h" class RimPolygon; +class Rim3dView; //================================================================================================== /// @@ -38,17 +39,26 @@ class RimPolygonContainer : public caf::PdmNestedCollection availableRealizationIdsForOverride() const; + + // Resolves an automatic "follow the view's own case" realization, only when the view's case + // genuinely matches this container's own data source. Returns -1 otherwise (fall back to this + // container's own default items). Default: always -1; only RimPolygonCloudAddress overrides + // this, to avoid applying an unrelated view's realization to a different field/ensemble. + virtual int resolveViewMatchingRealization( const Rim3dView* view ) const; + // Renames the polygon if another polygon in this container already carries the same name. void ensureUniquePolygonName( RimPolygon* polygon ); diff --git a/ApplicationLibCode/ProjectDataModel/Polygons/RimPolygonInViewCollection.cpp b/ApplicationLibCode/ProjectDataModel/Polygons/RimPolygonInViewCollection.cpp index 6134598f10..fc741e07a6 100644 --- a/ApplicationLibCode/ProjectDataModel/Polygons/RimPolygonInViewCollection.cpp +++ b/ApplicationLibCode/ProjectDataModel/Polygons/RimPolygonInViewCollection.cpp @@ -18,14 +18,22 @@ #include "RimPolygonInViewCollection.h" +#include "RiaDefines.h" + #include "ContourMap/RimEclipseContourMapView.h" #include "Rim3dView.h" +#include "RimGridView.h" #include "RimPolygon.h" #include "RimPolygonCollection.h" #include "RimPolygonInView.h" +#include "RimProject.h" #include "RimTools.h" +#include "Polygons/Cloud/RimPolygonCloudAddress.h" +#include "Polygons/Cloud/RimPolygonCloudSource.h" + #include "cafCmdFeatureMenuBuilder.h" +#include "cafPdmUiOrdering.h" CAF_PDM_SOURCE_INIT( RimPolygonInViewCollection, "RimPolygonInViewCollection" ); @@ -41,6 +49,12 @@ RimPolygonInViewCollection::RimPolygonInViewCollection() CAF_PDM_InitFieldNoDefault( &m_sourceCollection, "SourceCollection", "Source Collection" ); m_sourceCollection.uiCapability()->setUiHidden( true ); + CAF_PDM_InitField( &m_useAutoRealization, "UseAutoRealization", true, "Auto-Follow View Realization" ); + + CAF_PDM_InitField( &m_useAutoRealizationUiState, "UseAutoRealizationUiState", true, "Auto-Follow View Realization" ); + m_useAutoRealizationUiState.xmlCapability()->setIOWritable( false ); + m_useAutoRealizationUiState.xmlCapability()->setIOReadable( false ); + nameField()->uiCapability()->setUiHidden( true ); } @@ -53,6 +67,7 @@ void RimPolygonInViewCollection::updateFromPolygonCollection() { setSourceCollection( RimTools::polygonCollection() ); } + updateFromSource(); } @@ -134,6 +149,13 @@ void RimPolygonInViewCollection::fieldChangedByUi( const caf::PdmFieldHandle* ch if ( changedField == &m_isChecked ) { + if ( m_isChecked() ) + { + // Checking is the fetch trigger: re-run sync so prepareForSync() fetches (if needed) + // and sourceItems() mirrors the data this same pass. + updateFromSource(); + } + for ( auto poly : visiblePolygonsInView() ) { poly->updateConnectedEditors(); @@ -143,6 +165,22 @@ void RimPolygonInViewCollection::fieldChangedByUi( const caf::PdmFieldHandle* ch { view->scheduleCreateDisplayModelAndRedraw(); } + + // Unchecking only hides the node; it never evicts fetched data (that only happens via an + // explicit Reload or project reload). + } + else if ( changedField == &m_useAutoRealizationUiState ) + { + // Only reachable while editable (matching case), so this is always a genuine user edit. + m_useAutoRealization = m_useAutoRealizationUiState(); + + updateFromPolygonCollection(); + + if ( auto view = firstAncestorOfType() ) + { + view->updateViewTreeItems( RiaDefines::ItemIn3dView::POLYGON ); + view->scheduleCreateDisplayModelAndRedraw(); + } } } @@ -158,6 +196,48 @@ void RimPolygonInViewCollection::appendMenuItems( caf::CmdFeatureMenuBuilder& me RimPolygonCollection::appendPolygonMenuItems( menuBuilder ); } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimPolygonInViewCollection::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) +{ + // m_useAutoRealizationUiState is only meaningful on the mirror node for a whole + // RimPolygonCloudSource -- skipRemainingFields() below keeps it off every other node. + if ( auto* src = sourceCollection(); src && src->supportsRealizationOverride() ) + { + if ( viewMatchingRealizationOrMinusOne() == -1 ) + { + // Not meaningful here: force the checkbox to read unchecked, but leave the real, + // persisted m_useAutoRealization untouched so it re-applies once the case matches. + m_useAutoRealizationUiState = false; + m_useAutoRealizationUiState.uiCapability()->setUiReadOnly( true ); + m_useAutoRealizationUiState.uiCapability()->setUiToolTip( + "This view's case belongs to a different Sumo case/ensemble than this polygon " + "cloud source -- realization cannot be followed automatically. Using the polygon " + "source's own Applied realization instead." ); + } + else + { + m_useAutoRealizationUiState = m_useAutoRealization(); + m_useAutoRealizationUiState.uiCapability()->setUiReadOnly( false ); + m_useAutoRealizationUiState.uiCapability()->setUiToolTip( "" ); + } + + uiOrdering.add( &m_useAutoRealizationUiState ); + } + + uiOrdering.skipRemainingFields(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QList RimPolygonInViewCollection::calculateValueOptions( const caf::PdmFieldHandle* fieldNeedingOptions ) +{ + // No option-driven fields: m_useAutoRealization is a plain checkbox. + return {}; +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -168,12 +248,137 @@ std::vector RimPolygonInViewCollection::sourceSubCollectio } //-------------------------------------------------------------------------------------------------- -/// +/// Realization the owning view's case matches for the current source, or -1 if no match/no view. +//-------------------------------------------------------------------------------------------------- +int RimPolygonInViewCollection::viewMatchingRealizationOrMinusOne() const +{ + auto* src = sourceCollection(); + if ( !src ) return -1; + + auto* view = firstAncestorOfType(); + if ( !view ) return -1; + + return src->resolveViewMatchingRealization( view ); +} + +//-------------------------------------------------------------------------------------------------- +/// Items shown by this mirror: source's own items(), except a RimPolygonCloudAddress leaf +/// substitutes the cached data for this view's effective realization (see effectiveRealization()) +/// -- this is also the base-realization comparison mechanism. //-------------------------------------------------------------------------------------------------- std::vector RimPolygonInViewCollection::sourceItems() const { - if ( auto* src = sourceCollection() ) return src->items(); - return {}; + auto* src = sourceCollection(); + if ( !src ) return {}; + + if ( auto* address = dynamic_cast( src ) ) + { + const int realization = effectiveRealization(); + const int baseRealization = address->owningSource() ? address->owningSource()->baseRealization() : -1; + + if ( realization == -1 || realization == baseRealization ) return address->items(); + + return address->cachedItemsForRealization( realization ); + } + + return src->items(); +} + +//-------------------------------------------------------------------------------------------------- +/// Shows the effective realization in the tree name when it differs from the base realization +/// (e.g. this view is auto-following a different one). +//-------------------------------------------------------------------------------------------------- +QString RimPolygonInViewCollection::computeDisplayName() const +{ + auto* src = sourceCollection(); + if ( !src ) return {}; + + if ( auto* address = dynamic_cast( src ) ) + { + const int realization = effectiveRealization(); + const int baseRealization = address->owningSource() ? address->owningSource()->baseRealization() : -1; + + if ( realization != -1 && realization != baseRealization ) + { + return QString( "%1 (Real %2)" ).arg( address->name() ).arg( realization ); + } + } + else if ( auto* source = dynamic_cast( src ) ) + { + const int realization = effectiveRealization(); + + if ( realization != -1 && realization != source->baseRealization() ) + { + return source->nameForRealization( realization ); + } + } + + return src->collectionName(); +} + +//-------------------------------------------------------------------------------------------------- +/// Lazy-fetch trigger, run before sourceItems() this sync pass: if checked, ensures the address's +/// base realization is fetched (so it's always available in the project tree), plus the view's +/// effective (auto-followed) realization if different. Idempotent. +//-------------------------------------------------------------------------------------------------- +void RimPolygonInViewCollection::prepareForSync() +{ + auto* address = dynamic_cast( sourceCollection() ); + if ( !address || !m_isChecked() ) return; + + const int realization = effectiveRealization(); + const int baseRealization = address->owningSource() ? address->owningSource()->baseRealization() : -1; + + address->ensureBaseFetched(); + + if ( realization != -1 && realization != baseRealization ) + { + address->ensureRealizationFetched( realization ); + } +} + +//-------------------------------------------------------------------------------------------------- +/// New mirror nodes default to checked, except a RimPolygonCloudAddress leaf defaults to +/// unchecked since checking is its explicit fetch trigger. +//-------------------------------------------------------------------------------------------------- +RimPolygonInViewCollection* RimPolygonInViewCollection::createSubCollectionInView( RimPolygonContainer* src ) +{ + auto* sub = new RimPolygonInViewCollection(); + sub->setSourceCollection( src ); + + if ( dynamic_cast( src ) ) + { + sub->setCheckState( false ); + } + + return sub; +} + +//-------------------------------------------------------------------------------------------------- +/// Walks up to the ancestor mirror node (this included) whose sourceCollection() is a +/// RimPolygonCloudSource -- owner of the Auto-Follow checkbox. Nullptr if none. +//-------------------------------------------------------------------------------------------------- +const RimPolygonInViewCollection* RimPolygonInViewCollection::sourceMirrorAncestorOrThis() const +{ + const RimPolygonInViewCollection* node = this; + while ( node ) + { + if ( dynamic_cast( node->sourceCollection() ) ) return node; + node = node->firstAncestorOfType(); + } + return nullptr; +} + +//-------------------------------------------------------------------------------------------------- +/// Realization this node's items should show: the ancestor source's Auto-Follow-resolved +/// realization, or -1 (base/Applied) if off, mismatched, or no such ancestor. +//-------------------------------------------------------------------------------------------------- +int RimPolygonInViewCollection::effectiveRealization() const +{ + auto* sourceMirror = sourceMirrorAncestorOrThis(); + if ( !sourceMirror || !sourceMirror->m_useAutoRealization() ) return -1; + + return sourceMirror->viewMatchingRealizationOrMinusOne(); } //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/ProjectDataModel/Polygons/RimPolygonInViewCollection.h b/ApplicationLibCode/ProjectDataModel/Polygons/RimPolygonInViewCollection.h index 2b1a3e5630..d062d9b765 100644 --- a/ApplicationLibCode/ProjectDataModel/Polygons/RimPolygonInViewCollection.h +++ b/ApplicationLibCode/ProjectDataModel/Polygons/RimPolygonInViewCollection.h @@ -22,6 +22,8 @@ #include "RimPolygonContainer.h" #include "RimPolygonInView.h" +#include "cafPdmField.h" + class RimPolygon; //================================================================================================== @@ -35,6 +37,7 @@ class RimPolygonInViewCollection : public RimNestedMirrorCollectionInView visiblePolygonsInView() const; @@ -47,9 +50,44 @@ protected: std::vector sourceItems() const override; RimPolygonInView* createItemInView( RimPolygon* source ) override; + RimPolygonInViewCollection* createSubCollectionInView( RimPolygonContainer* src ) override; + + QString computeDisplayName() const override; + void prepareForSync() override; + + void defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) override; + QList calculateValueOptions( const caf::PdmFieldHandle* fieldNeedingOptions ) override; + private: RimPolygonInView* findPolygonInView( const RimPolygon* polygon ) const; void fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue ) override; void appendMenuItems( caf::CmdFeatureMenuBuilder& menuBuilder ) const override; + + // Per-view choice: follow the source container's own per-view realization resolution (see + // RimPolygonContainer::resolveViewMatchingRealization). Shown only on the mirror node for a + // whole RimPolygonCloudSource, governing every leaf beneath it. When checked and resolved to a + // realization other than the source's base one, every leaf substitutes that realization's + // cached data for its own base items (see sourceItems()) -- this is also the base-realization + // comparison mechanism. Disabled with a tooltip when the view's case doesn't match the + // source's data source. + caf::PdmField m_useAutoRealization; + + // Non-persisted UI-only shadow of m_useAutoRealization: when the checkbox is read-only + // (mismatched view case), it must visually read as unchecked even though the real field keeps + // its persisted value (so it re-applies once the case matches again). Synced with + // m_useAutoRealization whenever editable; user edits write back in fieldChangedByUi(). + caf::PdmField m_useAutoRealizationUiState; + + // Realization the owning view's case matches for the current source, or -1 if no match/no + // view ancestor. See RimPolygonContainer::resolveViewMatchingRealization. + int viewMatchingRealizationOrMinusOne() const; + + // Walks up to the ancestor mirror node (this included) whose sourceCollection() is a + // RimPolygonCloudSource -- the node owning the Auto-Follow checkbox. Nullptr if none. + const RimPolygonInViewCollection* sourceMirrorAncestorOrThis() const; + + // Realization this node's items should show: the ancestor source's Auto-Follow-resolved + // realization, or -1 (base/Applied) if Auto-Follow is off, mismatched, or no such ancestor. + int effectiveRealization() const; }; diff --git a/ApplicationLibCode/ProjectDataModel/RimNestedMirrorCollectionInView.h b/ApplicationLibCode/ProjectDataModel/RimNestedMirrorCollectionInView.h index 9f1dcab800..cd8418cde5 100644 --- a/ApplicationLibCode/ProjectDataModel/RimNestedMirrorCollectionInView.h +++ b/ApplicationLibCode/ProjectDataModel/RimNestedMirrorCollectionInView.h @@ -70,6 +70,19 @@ protected: // Default impl: new SelfT() + setSourceCollection(src). Override for extra wiring. virtual SelfT* createSubCollectionInView( SourceT* src ); + // Computes this mirror's display name from its source. Default: source->collectionName(). + // Override to incorporate per-view context (e.g. which realization is shown). + virtual QString computeDisplayName() const; + + // Called once per sync, per mirror node, before items/sub-collections are (re-)read from the + // source. Default: no-op. Override to lazily trigger a fetch (e.g. + // RimPolygonCloudAddress::ensureBaseFetched()) so it's picked up this same pass. + virtual void prepareForSync(); + + // Called once per sync, per mirror node, after name/items/sub-collections are refreshed. + // Default: no-op. Override for one-time-per-node bookkeeping. + virtual void onSynced(); + // Recursive sync: sub-collections, then items, then editor refresh. void updateAllViewItems(); diff --git a/ApplicationLibCode/ProjectDataModel/RimNestedMirrorCollectionInView.inl b/ApplicationLibCode/ProjectDataModel/RimNestedMirrorCollectionInView.inl index db2bf52e2b..2e6477a4a9 100644 --- a/ApplicationLibCode/ProjectDataModel/RimNestedMirrorCollectionInView.inl +++ b/ApplicationLibCode/ProjectDataModel/RimNestedMirrorCollectionInView.inl @@ -73,6 +73,31 @@ SelfT* RimNestedMirrorCollectionInView::createSubColl return sub; } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +template +QString RimNestedMirrorCollectionInView::computeDisplayName() const +{ + return m_sourceCollection ? m_sourceCollection->collectionName() : QString(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +template +void RimNestedMirrorCollectionInView::prepareForSync() +{ +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +template +void RimNestedMirrorCollectionInView::onSynced() +{ +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -100,21 +125,19 @@ ItemViewT* RimNestedMirrorCollectionInView::findItemI } //-------------------------------------------------------------------------------------------------- -/// Recursive sync of sub-collections and items, mirroring the source tree. -/// -/// Step 1 - sub-collections: drop wrappers whose source pointer is null, then iterate -/// sourceSubCollections() and find-or-create one wrapper per source, reordering to match. -/// Recurse into each child so nested levels mirror as well. -/// -/// Step 2 - items: same shape. Drop orphans, iterate sourceItems(), find-or-create. +/// Recursive sync mirroring the source tree: drops orphaned wrappers, then find-or-creates one +/// wrapper per source sub-collection/item, reordering to match, recursing into children. //-------------------------------------------------------------------------------------------------- template void RimNestedMirrorCollectionInView::updateAllViewItems() { - // Refresh display name from source so the view tree label tracks the source. + // Let the derived class lazily trigger a fetch before sourceItems() is read below. + this->prepareForSync(); + + // Refresh display name from source. if ( m_sourceCollection ) { - this->setName( m_sourceCollection->collectionName() ); + this->setName( this->computeDisplayName() ); } // --- Sub-collections --- @@ -188,5 +211,6 @@ void RimNestedMirrorCollectionInView::updateAllViewIt } } + this->onSynced(); this->updateConnectedEditors(); } diff --git a/ApplicationLibCode/ProjectDataModel/RimRoffCaseSumo.cpp b/ApplicationLibCode/ProjectDataModel/RimRoffCaseSumo.cpp index ec209ceb27..e401ddab74 100644 --- a/ApplicationLibCode/ProjectDataModel/RimRoffCaseSumo.cpp +++ b/ApplicationLibCode/ProjectDataModel/RimRoffCaseSumo.cpp @@ -190,6 +190,14 @@ int RimRoffCaseSumo::realization() const return m_realization(); } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimSumoDataSource* RimRoffCaseSumo::dataSource() const +{ + return m_sumoDataSource(); +} + //-------------------------------------------------------------------------------------------------- /// Every realization downloads and parses its own grid blob, because a roff grid carries its geometry and /// its active cells in one stream and there is no way to read the active cells alone. When the ensemble diff --git a/ApplicationLibCode/ProjectDataModel/RimRoffCaseSumo.h b/ApplicationLibCode/ProjectDataModel/RimRoffCaseSumo.h index 887dbf7219..0d44dcfd78 100644 --- a/ApplicationLibCode/ProjectDataModel/RimRoffCaseSumo.h +++ b/ApplicationLibCode/ProjectDataModel/RimRoffCaseSumo.h @@ -70,6 +70,11 @@ public: QString gridName() const; int realization() const; + // Data source this case was created from -- used to check whether a view's case belongs to + // the same Sumo case/ensemble before auto-following that view's realization. See + // RimPolygonCloudSource::resolveViewMatchingRealization. + RimSumoDataSource* dataSource() const; + bool openEclipseGridFile() override; void closeReservoirCase() override; diff --git a/Fwk/AppFwk/cafProjectDataModel/cafPdmUiCore/CMakeLists.txt b/Fwk/AppFwk/cafProjectDataModel/cafPdmUiCore/CMakeLists.txt index 27549d5595..241960dfd9 100644 --- a/Fwk/AppFwk/cafProjectDataModel/cafPdmUiCore/CMakeLists.txt +++ b/Fwk/AppFwk/cafProjectDataModel/cafPdmUiCore/CMakeLists.txt @@ -55,6 +55,8 @@ set(PROJECT_FILES cafPdmUiLabel.h cafPdmUiButton.cpp cafPdmUiButton.h + cafPdmUiButtonBox.cpp + cafPdmUiButtonBox.h cafSelectionManager.cpp cafSelectionManager.h cafSelectionChangedReceiver.h diff --git a/Fwk/AppFwk/cafProjectDataModel/cafPdmUiCore/cafPdmUiButtonBox.cpp b/Fwk/AppFwk/cafProjectDataModel/cafPdmUiCore/cafPdmUiButtonBox.cpp new file mode 100644 index 0000000000..746fc7a5e2 --- /dev/null +++ b/Fwk/AppFwk/cafProjectDataModel/cafPdmUiCore/cafPdmUiButtonBox.cpp @@ -0,0 +1,76 @@ +//################################################################################################## +// +// Custom Visualization Core library +// Copyright (C) 2026 Equinor ASA +// +// This library may be used under the terms of either the GNU General Public License or +// the GNU Lesser General Public License as follows: +// +// GNU General Public License Usage +// This library 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. +// +// This library 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. +// +// GNU Lesser General Public License Usage +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library 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 Lesser General Public License at <> +// for more details. +// +//################################################################################################## + +#include "cafPdmUiButtonBox.h" + +namespace caf +{ +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +PdmUiButtonBox::PdmUiButtonBox() +{ + // No separate label -- the box itself fills the whole row/cell it is given, like a + // QDialogButtonBox does inside a real QDialog. + setUiLabelPosition( PdmUiItemInfo::LabelPosition::HIDDEN ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +PdmUiButtonBox::ButtonSpec& PdmUiButtonBox::addButton( const QString& text, const ClickCallback& callback ) +{ + m_buttons.push_back( { text, callback, true, QString() } ); + return m_buttons.back(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +const std::list& PdmUiButtonBox::buttons() const +{ + return m_buttons; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool PdmUiButtonBox::isUiGroup() const +{ + return false; +} + +} // End of namespace caf diff --git a/Fwk/AppFwk/cafProjectDataModel/cafPdmUiCore/cafPdmUiButtonBox.h b/Fwk/AppFwk/cafProjectDataModel/cafPdmUiCore/cafPdmUiButtonBox.h new file mode 100644 index 0000000000..b2b1a6649e --- /dev/null +++ b/Fwk/AppFwk/cafProjectDataModel/cafPdmUiCore/cafPdmUiButtonBox.h @@ -0,0 +1,80 @@ +//################################################################################################## +// +// Custom Visualization Core library +// Copyright (C) 2026 Equinor ASA +// +// This library may be used under the terms of either the GNU General Public License or +// the GNU Lesser General Public License as follows: +// +// GNU General Public License Usage +// This library 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. +// +// This library 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. +// +// GNU Lesser General Public License Usage +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library 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 Lesser General Public License at <> +// for more details. +// +//################################################################################################## + +#pragma once + +#include "cafPdmUiItem.h" + +#include + +#include +#include + +namespace caf +{ +//================================================================================================== +/// A row of action buttons (e.g. "Apply"/"Cancel") laid out using a real QDialogButtonBox, not +/// tied to a PDM field. Mirrors the native button-box look (packed, pinned to one side) used by +/// QDialogs, for buttons embedded directly in an inline property-panel form. +//================================================================================================== +class PdmUiButtonBox : public PdmUiItem +{ +public: + using ClickCallback = std::function; + + struct ButtonSpec + { + QString text; + ClickCallback callback; + bool enabled{ true }; + QString toolTip; + }; + + PdmUiButtonBox(); + + // Returned reference is valid for the lifetime of this instance -- used to update enabled + // state/tooltip later in the same defineUiOrdering() call. + ButtonSpec& addButton( const QString& text, const ClickCallback& callback ); + + const std::list& buttons() const; + + bool isUiGroup() const override; + +private: + std::list m_buttons; +}; + +} // End of namespace caf diff --git a/Fwk/AppFwk/cafProjectDataModel/cafPdmUiCore/cafPdmUiGroup.cpp b/Fwk/AppFwk/cafProjectDataModel/cafPdmUiCore/cafPdmUiGroup.cpp index 13c6904eb8..da53908c12 100644 --- a/Fwk/AppFwk/cafProjectDataModel/cafPdmUiCore/cafPdmUiGroup.cpp +++ b/Fwk/AppFwk/cafProjectDataModel/cafPdmUiCore/cafPdmUiGroup.cpp @@ -47,6 +47,7 @@ PdmUiGroup::PdmUiGroup() m_hasForcedExpandedState = false; m_forcedCollapseState = false; m_enableFrame = true; + m_tightHorizontalSpacing = false; } //-------------------------------------------------------------------------------------------------- @@ -104,6 +105,14 @@ void PdmUiGroup::setEnableFrame( bool enableFrame ) m_enableFrame = enableFrame; } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void PdmUiGroup::setTightHorizontalSpacing( bool enable ) +{ + m_tightHorizontalSpacing = enable; +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -136,4 +145,12 @@ bool PdmUiGroup::enableFrame() const return m_enableFrame; } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool PdmUiGroup::tightHorizontalSpacing() const +{ + return m_tightHorizontalSpacing; +} + } // End of namespace caf diff --git a/Fwk/AppFwk/cafProjectDataModel/cafPdmUiCore/cafPdmUiGroup.h b/Fwk/AppFwk/cafProjectDataModel/cafPdmUiCore/cafPdmUiGroup.h index 3b4e7b7b54..ce9c3c5852 100644 --- a/Fwk/AppFwk/cafProjectDataModel/cafPdmUiCore/cafPdmUiGroup.h +++ b/Fwk/AppFwk/cafProjectDataModel/cafPdmUiCore/cafPdmUiGroup.h @@ -62,17 +62,22 @@ public: void setCollapsed( bool doCollapse ); void setEnableFrame( bool enableFrame ); + /// Zero horizontal spacing between items -- for a frameless group laying out buttons snugly. + void setTightHorizontalSpacing( bool enable = true ); + // Pdm internal methods bool isExpandedByDefault() const; bool hasForcedExpandedState() const; bool forcedExpandedState() const; bool enableFrame() const; + bool tightHorizontalSpacing() const; private: bool m_isCollapsedByDefault; bool m_hasForcedExpandedState; bool m_forcedCollapseState; bool m_enableFrame; + bool m_tightHorizontalSpacing; QString m_keyword; }; diff --git a/Fwk/AppFwk/cafProjectDataModel/cafPdmUiCore/cafPdmUiOrdering.cpp b/Fwk/AppFwk/cafProjectDataModel/cafPdmUiCore/cafPdmUiOrdering.cpp index 6fe912b620..a1d59e5b20 100644 --- a/Fwk/AppFwk/cafProjectDataModel/cafPdmUiCore/cafPdmUiOrdering.cpp +++ b/Fwk/AppFwk/cafProjectDataModel/cafPdmUiCore/cafPdmUiOrdering.cpp @@ -38,6 +38,7 @@ #include "cafPdmObjectHandle.h" #include "cafPdmUiButton.h" +#include "cafPdmUiButtonBox.h" #include "cafPdmUiFieldHandle.h" #include "cafPdmUiLabel.h" #include "cafPdmUiObjectHandle.h" @@ -100,6 +101,19 @@ PdmUiButton* return m_createdButtons.back().get(); } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +PdmUiButtonBox* PdmUiOrdering::addNewButtonBox( LayoutOptions layout ) +{ + auto buttonBox = std::make_unique(); + + m_ordering.emplace_back( buttonBox.get(), layout ); + m_createdButtonBoxes.push_back( std::move( buttonBox ) ); + + return m_createdButtonBoxes.back().get(); +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- diff --git a/Fwk/AppFwk/cafProjectDataModel/cafPdmUiCore/cafPdmUiOrdering.h b/Fwk/AppFwk/cafProjectDataModel/cafPdmUiCore/cafPdmUiOrdering.h index 34bba0e898..4e15d4d554 100644 --- a/Fwk/AppFwk/cafProjectDataModel/cafPdmUiCore/cafPdmUiOrdering.h +++ b/Fwk/AppFwk/cafProjectDataModel/cafPdmUiCore/cafPdmUiOrdering.h @@ -49,6 +49,7 @@ namespace caf class PdmUiGroup; class PdmUiLabel; class PdmUiButton; +class PdmUiButtonBox; class PdmFieldHandle; class PdmObjectHandle; @@ -95,11 +96,12 @@ public: const PdmFieldHandle* fieldToInsert, LayoutOptions layout = defaultLayoutOptions() ); - PdmUiGroup* addNewGroup( const QString& displayName, LayoutOptions layout = defaultLayoutOptions() ); - PdmUiLabel* addNewLabel( const QString& labelText, LayoutOptions layout = defaultLayoutOptions() ); - PdmUiButton* addNewButton( const QString& buttonText, - const std::function& callback, - LayoutOptions layout = defaultLayoutOptions() ); + PdmUiGroup* addNewGroup( const QString& displayName, LayoutOptions layout = defaultLayoutOptions() ); + PdmUiLabel* addNewLabel( const QString& labelText, LayoutOptions layout = defaultLayoutOptions() ); + PdmUiButton* addNewButton( const QString& buttonText, + const std::function& callback, + LayoutOptions layout = defaultLayoutOptions() ); + PdmUiButtonBox* addNewButtonBox( LayoutOptions layout = defaultLayoutOptions() ); PdmUiGroup* createGroupBeforeGroup( const QString& groupId, const QString& displayName, @@ -162,11 +164,12 @@ private: const QString& groupKeyword, LayoutOptions layout = defaultLayoutOptions() ); - std::vector m_ordering; - std::vector> m_createdGroups; - std::vector> m_createdLabels; - std::vector> m_createdButtons; - bool m_skipRemainingFields; + std::vector m_ordering; + std::vector> m_createdGroups; + std::vector> m_createdLabels; + std::vector> m_createdButtons; + std::vector> m_createdButtonBoxes; + bool m_skipRemainingFields; }; } // End of namespace caf diff --git a/Fwk/AppFwk/cafUserInterface/cafPdmUiFormLayoutObjectEditor.cpp b/Fwk/AppFwk/cafUserInterface/cafPdmUiFormLayoutObjectEditor.cpp index 2078ed1ffa..055c40e74f 100644 --- a/Fwk/AppFwk/cafUserInterface/cafPdmUiFormLayoutObjectEditor.cpp +++ b/Fwk/AppFwk/cafUserInterface/cafPdmUiFormLayoutObjectEditor.cpp @@ -41,6 +41,7 @@ #include "cafPdmLogging.h" #include "cafPdmObjectHandle.h" #include "cafPdmUiButton.h" +#include "cafPdmUiButtonBox.h" #include "cafPdmUiFieldEditorHandle.h" #include "cafPdmUiFieldEditorHelper.h" #include "cafPdmUiFieldHandle.h" @@ -55,6 +56,7 @@ #include "QMinimizePanel.h" #include +#include #include #include #include @@ -235,6 +237,14 @@ int caf::PdmUiFormLayoutObjectEditor::recursivelyConfigureAndUpdateUiOrderingInG .arg( button->uiName( uiConfigName ) ) ); } } + else if ( auto* buttonBox = dynamic_cast( currentItem ) ) + { + // No alignment: stretched to fill its cell, its own internal layout packs the + // buttons -- the same mechanism a real QDialog's button row uses. + QDialogButtonBox* qButtonBox = createButtonBox( containerWidgetWithGridLayout, *buttonBox ); + parentLayout->addWidget( qButtonBox, currentRowIndex, currentColumn, 1, itemColumnSpan ); + currentColumn += itemColumnSpan; + } else { PdmUiFieldEditorHandle* fieldEditor = nullptr; @@ -488,6 +498,8 @@ QPushButton* caf::PdmUiFormLayoutObjectEditor::createButton( QWidget* { QPushButton* qButton = new QPushButton( parent ); qButton->setText( button.uiName( uiConfigName ) ); + qButton->setEnabled( !button.isUiReadOnly( uiConfigName ) ); + qButton->setToolTip( button.uiToolTip( uiConfigName ) ); auto hPolicy = button.fillWidth() ? QSizePolicy::Expanding : QSizePolicy::Maximum; qButton->setSizePolicy( hPolicy, QSizePolicy::Fixed ); @@ -510,6 +522,32 @@ QPushButton* caf::PdmUiFormLayoutObjectEditor::createButton( QWidget* return qButton; } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QDialogButtonBox* caf::PdmUiFormLayoutObjectEditor::createButtonBox( QWidget* parent, const PdmUiButtonBox& buttonBox ) +{ + QDialogButtonBox* qButtonBox = new QDialogButtonBox( parent ); + + // ActionRole preserves add order (no OS-specific OK/Cancel reordering) while still getting + // QDialogButtonBox's normal leading-stretch-then-packed-buttons layout. + for ( const auto& spec : buttonBox.buttons() ) + { + QPushButton* qButton = qButtonBox->addButton( spec.text, QDialogButtonBox::ActionRole ); + qButton->setEnabled( spec.enabled ); + qButton->setToolTip( spec.toolTip ); + + auto callback = spec.callback; + if ( callback ) + { + QObject::connect( qButton, &QPushButton::clicked, [callback]() { callback(); } ); + } + } + + m_buttonBoxes.push_back( qButtonBox ); + return qButtonBox; +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -576,7 +614,8 @@ caf::PdmUiFieldEditorHandle* caf::PdmUiFormLayoutObjectEditor::findOrCreateField /// //-------------------------------------------------------------------------------------------------- void caf::PdmUiFormLayoutObjectEditor::ensureWidgetContainsEmptyGridLayout( QWidget* containerWidget, - QMargins contentMargins ) + QMargins contentMargins, + int horizontalSpacing ) { CAF_ASSERT( containerWidget ); QLayout* layout = containerWidget->layout(); @@ -594,6 +633,10 @@ void caf::PdmUiFormLayoutObjectEditor::ensureWidgetContainsEmptyGridLayout( QWid QGridLayout* gridLayout = new QGridLayout; gridLayout->setContentsMargins( contentMargins ); + if ( horizontalSpacing >= 0 ) + { + gridLayout->setHorizontalSpacing( horizontalSpacing ); + } containerWidget->setLayout( gridLayout ); } @@ -684,6 +727,15 @@ void caf::PdmUiFormLayoutObjectEditor::deleteLabelsAndButtons() } } m_buttons.clear(); + + for ( auto& buttonBox : m_buttonBoxes ) + { + if ( buttonBox ) + { + buttonBox->deleteLater(); + } + } + m_buttonBoxes.clear(); } //-------------------------------------------------------------------------------------------------- diff --git a/Fwk/AppFwk/cafUserInterface/cafPdmUiFormLayoutObjectEditor.h b/Fwk/AppFwk/cafUserInterface/cafPdmUiFormLayoutObjectEditor.h index dfe8869ece..f0149008c2 100644 --- a/Fwk/AppFwk/cafUserInterface/cafPdmUiFormLayoutObjectEditor.h +++ b/Fwk/AppFwk/cafUserInterface/cafPdmUiFormLayoutObjectEditor.h @@ -50,6 +50,7 @@ class QWidget; class QVBoxLayout; class QLabel; class QPushButton; +class QDialogButtonBox; namespace caf { @@ -59,6 +60,7 @@ class PdmUiFieldHandle; class PdmUiGroup; class PdmUiLabel; class PdmUiButton; +class PdmUiButtonBox; class PdmUiOrdering; //================================================================================================== @@ -97,9 +99,12 @@ protected: QMinimizePanel* findOrCreateGroupBox( QWidget* parent, PdmUiGroup* group, const QString& uiConfigName ); PdmUiFieldEditorHandle* findOrCreateFieldEditor( QWidget* parent, PdmUiFieldHandle* field, const QString& uiConfigName ); - QPushButton* createButton( QWidget* parent, const PdmUiButton& button, const QString& uiConfigName ); + QPushButton* createButton( QWidget* parent, const PdmUiButton& button, const QString& uiConfigName ); + QDialogButtonBox* createButtonBox( QWidget* parent, const PdmUiButtonBox& buttonBox ); - static void ensureWidgetContainsEmptyGridLayout( QWidget* containerWidget, QMargins contentMargins = QMargins() ); + static void ensureWidgetContainsEmptyGridLayout( QWidget* containerWidget, + QMargins contentMargins = QMargins(), + int horizontalSpacing = -1 ); private slots: void groupBoxExpandedStateToggled( bool isExpanded ); @@ -124,8 +129,9 @@ private: std::map> m_groupBoxes; std::map> m_newGroupBoxes; ///< used temporarily to store the new(complete) set of ///< group boxes - std::vector> m_labels; - std::vector> m_buttons; + std::vector> m_labels; + std::vector> m_buttons; + std::vector> m_buttonBoxes; std::map> m_objectKeywordGroupUiNameExpandedState; }; diff --git a/Fwk/AppFwk/cafUserInterface/cafPdmUiTreeAttributes.h b/Fwk/AppFwk/cafUserInterface/cafPdmUiTreeAttributes.h index 09a5916278..ac6de43db7 100644 --- a/Fwk/AppFwk/cafUserInterface/cafPdmUiTreeAttributes.h +++ b/Fwk/AppFwk/cafUserInterface/cafPdmUiTreeAttributes.h @@ -70,6 +70,7 @@ public: QColor fgColor; bool selectedOnly; QRect rect; + QString toolTip; caf::Signal clicked; diff --git a/Fwk/AppFwk/cafUserInterface/cafPdmUiTreeViewItemDelegate.cpp b/Fwk/AppFwk/cafUserInterface/cafPdmUiTreeViewItemDelegate.cpp index bb51f4d088..1a01d342de 100644 --- a/Fwk/AppFwk/cafUserInterface/cafPdmUiTreeViewItemDelegate.cpp +++ b/Fwk/AppFwk/cafUserInterface/cafPdmUiTreeViewItemDelegate.cpp @@ -51,11 +51,13 @@ #include #include +#include #include #include #include #include #include +#include #include namespace caf @@ -279,6 +281,28 @@ bool PdmUiTreeViewItemDelegate::editorEvent( QEvent* event, return QStyledItemDelegate::editorEvent( event, model, option, itemIndex ); } +//-------------------------------------------------------------------------------------------------- +/// Shows a tag's own tooltip text (if it has one) when the mouse hovers over that tag's rect, +/// instead of falling back to the whole tree item's tooltip. +//-------------------------------------------------------------------------------------------------- +bool PdmUiTreeViewItemDelegate::helpEvent( QHelpEvent* event, + QAbstractItemView* view, + const QStyleOptionViewItem& option, + const QModelIndex& itemIndex ) +{ + if ( event && event->type() == QEvent::ToolTip ) + { + const PdmUiTreeViewItemAttribute::Tag* tag = nullptr; + if ( tagClicked( event->pos(), option.rect, itemIndex, &tag ) && tag && !tag->toolTip.isEmpty() ) + { + QToolTip::showText( event->globalPos(), tag->toolTip, view ); + return true; + } + } + + return QStyledItemDelegate::helpEvent( event, view, option, itemIndex ); +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- diff --git a/Fwk/AppFwk/cafUserInterface/cafPdmUiTreeViewItemDelegate.h b/Fwk/AppFwk/cafUserInterface/cafPdmUiTreeViewItemDelegate.h index 5c442e7914..745d49979e 100644 --- a/Fwk/AppFwk/cafUserInterface/cafPdmUiTreeViewItemDelegate.h +++ b/Fwk/AppFwk/cafUserInterface/cafPdmUiTreeViewItemDelegate.h @@ -43,6 +43,7 @@ #include #include +#include #include #include #include @@ -69,6 +70,10 @@ protected: QAbstractItemModel* model, const QStyleOptionViewItem& option, const QModelIndex& itemIndex ) override; + bool helpEvent( QHelpEvent* event, + QAbstractItemView* view, + const QStyleOptionViewItem& option, + const QModelIndex& itemIndex ) override; bool tagClicked( const QPoint& clickPos, const QRect& itemRect, const QModelIndex& itemIndex, diff --git a/scripts/ri-cloud-api b/scripts/ri-cloud-api index f136041f74..08190ab163 160000 --- a/scripts/ri-cloud-api +++ b/scripts/ri-cloud-api @@ -1 +1 @@ -Subproject commit f136041f74840964664ea3173281cce9b64457ca +Subproject commit 08190ab1630aab6fa20333dae50e1f04e37d0df4