From 6b1b89dae526ea4465bdbbae3ee8a95e9c774a0f Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 18 Apr 2024 13:24:49 +1000 Subject: [PATCH] [feature] Add "View Output Layers" option for model child algorithms When editing a model through the designer (and after having run that model), you can now right click any child step in the model and select "View Output Layers". This will add the output layers from that step as new layers in the current QGIS project. This action is available for ALL child algorithms in the model, even if the model is not configured to use the outputs from those children as model outputs. This is designed as a helpful debugging action. If a user's model fails (or gives unexpected results), they can then trace through the model and view the outputs for suspected problematic steps. It avoids the need to add temporary outputs to a model and re-run to test. Additionally, this action is always available after running the model, EVEN if the model itself failed (eg because of a misconfigured step later in the model). Sponsored by City of Canning --- .../qgsmodelcomponentgraphicitem.sip.in | 9 +++ .../models/qgsmodelgraphicsscene.sip.in | 7 ++ .../qgsmodelcomponentgraphicitem.sip.in | 9 +++ .../models/qgsmodelgraphicsscene.sip.in | 7 ++ .../models/qgsmodelcomponentgraphicitem.cpp | 15 ++++ .../models/qgsmodelcomponentgraphicitem.h | 9 +++ .../models/qgsmodeldesignerdialog.cpp | 79 +++++++++++++++++++ .../models/qgsmodeldesignerdialog.h | 1 + .../models/qgsmodelgraphicsscene.cpp | 12 ++- .../processing/models/qgsmodelgraphicsscene.h | 7 ++ 10 files changed, 152 insertions(+), 3 deletions(-) diff --git a/python/PyQt6/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in b/python/PyQt6/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in index ec898e937c04..e58f4674f619 100644 --- a/python/PyQt6/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in +++ b/python/PyQt6/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in @@ -404,6 +404,15 @@ Sets the results obtained for this child algorithm for the last model execution void setInputs( const QVariantMap &inputs ); %Docstring Sets the inputs used for this child algorithm for the last model execution through the dialog. +%End + + signals: + + void showPreviousResults(); +%Docstring +Emitted when the user opts to view previous results from this child algorithm. + +.. versionadded:: 3.38 %End protected: diff --git a/python/PyQt6/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in b/python/PyQt6/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in index eca0686d0370..aa6648a85e11 100644 --- a/python/PyQt6/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in +++ b/python/PyQt6/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in @@ -177,6 +177,13 @@ Emitted whenever a component of the model is changed. %Docstring Emitted whenever the selected item changes. If ``None``, no item is selected. +%End + + void showPreviousResults( const QString &childId ); +%Docstring +Emitted when the user opts to view previous results from the child algorithm with matching ID. + +.. versionadded:: 3.38 %End protected: diff --git a/python/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in b/python/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in index 9295a8196835..ff591ffb348a 100644 --- a/python/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in +++ b/python/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in @@ -404,6 +404,15 @@ Sets the results obtained for this child algorithm for the last model execution void setInputs( const QVariantMap &inputs ); %Docstring Sets the inputs used for this child algorithm for the last model execution through the dialog. +%End + + signals: + + void showPreviousResults(); +%Docstring +Emitted when the user opts to view previous results from this child algorithm. + +.. versionadded:: 3.38 %End protected: diff --git a/python/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in b/python/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in index f625089a41f1..9e9373749dc1 100644 --- a/python/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in +++ b/python/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in @@ -177,6 +177,13 @@ Emitted whenever a component of the model is changed. %Docstring Emitted whenever the selected item changes. If ``None``, no item is selected. +%End + + void showPreviousResults( const QString &childId ); +%Docstring +Emitted when the user opts to view previous results from the child algorithm with matching ID. + +.. versionadded:: 3.38 %End protected: diff --git a/src/gui/processing/models/qgsmodelcomponentgraphicitem.cpp b/src/gui/processing/models/qgsmodelcomponentgraphicitem.cpp index 138e4aa31334..02cbb26bfdda 100644 --- a/src/gui/processing/models/qgsmodelcomponentgraphicitem.cpp +++ b/src/gui/processing/models/qgsmodelcomponentgraphicitem.cpp @@ -890,6 +890,21 @@ void QgsModelChildAlgorithmGraphicItem::contextMenuEvent( QGraphicsSceneContextM QAction *deactivateAction = popupmenu->addAction( QObject::tr( "Deactivate" ) ); connect( deactivateAction, &QAction::triggered, this, &QgsModelChildAlgorithmGraphicItem::deactivateAlgorithm ); } + + // only show the "View Output Layers" action for algorithms which create layers + if ( const QgsProcessingAlgorithm *algorithm = child->algorithm() ) + { + const QList< const QgsProcessingParameterDefinition * > outputParams = algorithm->destinationParameterDefinitions(); + if ( !outputParams.isEmpty() ) + { + popupmenu->addSeparator(); + QAction *viewOutputLayersAction = popupmenu->addAction( QObject::tr( "View Output Layers" ) ); + viewOutputLayersAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mActionShowSelectedLayers.svg" ) ) ); + connect( viewOutputLayersAction, &QAction::triggered, this, &QgsModelChildAlgorithmGraphicItem::showPreviousResults ); + if ( mResults.empty() ) + viewOutputLayersAction->setEnabled( false ); + } + } } popupmenu->exec( event->screenPos() ); diff --git a/src/gui/processing/models/qgsmodelcomponentgraphicitem.h b/src/gui/processing/models/qgsmodelcomponentgraphicitem.h index c3cb7c23f286..2499526c2756 100644 --- a/src/gui/processing/models/qgsmodelcomponentgraphicitem.h +++ b/src/gui/processing/models/qgsmodelcomponentgraphicitem.h @@ -470,6 +470,15 @@ class GUI_EXPORT QgsModelChildAlgorithmGraphicItem : public QgsModelComponentGra */ void setInputs( const QVariantMap &inputs ); + signals: + + /** + * Emitted when the user opts to view previous results from this child algorithm. + * + * \since QGIS 3.38 + */ + void showPreviousResults(); + protected: QColor fillColor( State state ) const override; diff --git a/src/gui/processing/models/qgsmodeldesignerdialog.cpp b/src/gui/processing/models/qgsmodeldesignerdialog.cpp index dd3e22bcc6dc..8cc5168cae04 100644 --- a/src/gui/processing/models/qgsmodeldesignerdialog.cpp +++ b/src/gui/processing/models/qgsmodeldesignerdialog.cpp @@ -39,6 +39,7 @@ #include "qgsscreenhelper.h" #include "qgsmessagelog.h" #include "qgsprocessingalgorithmdialogbase.h" +#include "qgsproject.h" #include #include #include @@ -511,6 +512,7 @@ void QgsModelDesignerDialog::setModelScene( QgsModelGraphicsScene *scene ) } ); connect( mScene, &QgsModelGraphicsScene::componentAboutToChange, this, [ = ]( const QString & description, int id ) { beginUndoCommand( description, id ); } ); connect( mScene, &QgsModelGraphicsScene::componentChanged, this, [ = ] { endUndoCommand(); } ); + connect( mScene, &QgsModelGraphicsScene::showPreviousResults, this, &QgsModelDesignerDialog::showPreviousResults ); mView->centerOn( center ); @@ -1048,6 +1050,83 @@ void QgsModelDesignerDialog::run() dialog->exec(); } +void QgsModelDesignerDialog::showPreviousResults( const QString &childId ) +{ + const QString childDescription = mModel->childAlgorithm( childId ).description(); + + const QVariantMap childAlgorithmResults = mChildResults.value( childId ).toMap(); + if ( childAlgorithmResults.isEmpty() ) + { + mMessageBar->pushWarning( QString(), tr( "No results are available for %1" ).arg( childDescription ) ); + return; + } + + const QgsProcessingAlgorithm *algorithm = mModel->childAlgorithm( childId ).algorithm(); + if ( !algorithm ) + { + mMessageBar->pushCritical( QString(), tr( "Results cannot be shown for an invalid model component" ) ); + return; + } + + const QList< const QgsProcessingParameterDefinition * > outputParams = algorithm->destinationParameterDefinitions(); + if ( outputParams.isEmpty() ) + { + // this situation should not arise in normal use, we don't show the action in this case + QgsDebugError( "Cannot show results for algorithms with no outputs" ); + return; + } + + bool foundResults = false; + for ( const QgsProcessingParameterDefinition *outputParam : outputParams ) + { + const QVariant output = childAlgorithmResults.value( outputParam->name() ); + if ( !output.isValid() ) + continue; + + if ( output.type() == QVariant::String ) + { + if ( QgsMapLayer *resultLayer = QgsProcessingUtils::mapLayerFromString( output.toString(), mLayerStore ) ) + { + QgsDebugMsgLevel( QStringLiteral( "Loading previous result for %1: %2" ).arg( outputParam->name(), output.toString() ), 2 ); + + std::unique_ptr< QgsMapLayer > layer( resultLayer->clone() ); + + QString baseName; + if ( outputParams.size() > 1 ) + baseName = tr( "%1 — %2" ).arg( childDescription, outputParam->name() ); + else + baseName = childDescription; + + // make name unique, so that's it's easy to see which is the most recent result. + // (this helps when running the model multiple times.) + QString name = baseName; + int counter = 1; + while ( !QgsProject::instance()->mapLayersByName( name ).empty() ) + { + counter += 1; + name = tr( "%1 (%2)" ).arg( baseName ).arg( counter ); + } + + layer->setName( name ); + + QgsProject::instance()->addMapLayer( layer.release() ); + foundResults = true; + } + else + { + // should not happen in normal operation + QgsDebugError( QStringLiteral( "Could not load previous result for %1: %2" ).arg( outputParam->name(), output.toString() ) ); + } + } + } + + if ( !foundResults ) + { + mMessageBar->pushWarning( QString(), tr( "No results are available for %1" ).arg( childDescription ) ); + return; + } +} + void QgsModelDesignerDialog::validate() { QStringList issues; diff --git a/src/gui/processing/models/qgsmodeldesignerdialog.h b/src/gui/processing/models/qgsmodeldesignerdialog.h index 7097fdc28bc8..256d8bc482b0 100644 --- a/src/gui/processing/models/qgsmodeldesignerdialog.h +++ b/src/gui/processing/models/qgsmodeldesignerdialog.h @@ -190,6 +190,7 @@ class GUI_EXPORT QgsModelDesignerDialog : public QMainWindow, public Ui::QgsMode void setPanelVisibility( bool hidden ); void editHelp(); void run(); + void showPreviousResults( const QString &childId ); private: diff --git a/src/gui/processing/models/qgsmodelgraphicsscene.cpp b/src/gui/processing/models/qgsmodelgraphicsscene.cpp index b9c712750fe7..359d82981017 100644 --- a/src/gui/processing/models/qgsmodelgraphicsscene.cpp +++ b/src/gui/processing/models/qgsmodelgraphicsscene.cpp @@ -138,12 +138,18 @@ void QgsModelGraphicsScene::createItems( QgsProcessingModelAlgorithm *model, Qgs QgsModelChildAlgorithmGraphicItem *item = createChildAlgGraphicItem( model, it.value().clone() ); addItem( item ); item->setPos( it.value().position().x(), it.value().position().y() ); - item->setResults( mChildResults.value( it.value().childId() ).toMap() ); - item->setInputs( mChildInputs.value( it.value().childId() ).toMap() ); - mChildAlgorithmItems.insert( it.value().childId(), item ); + + const QString childId = it.value().childId(); + item->setResults( mChildResults.value( childId ).toMap() ); + item->setInputs( mChildInputs.value( childId ).toMap() ); + mChildAlgorithmItems.insert( childId, item ); connect( item, &QgsModelComponentGraphicItem::requestModelRepaint, this, &QgsModelGraphicsScene::rebuildRequired ); connect( item, &QgsModelComponentGraphicItem::changed, this, &QgsModelGraphicsScene::componentChanged ); connect( item, &QgsModelComponentGraphicItem::aboutToChange, this, &QgsModelGraphicsScene::componentAboutToChange ); + connect( item, &QgsModelChildAlgorithmGraphicItem::showPreviousResults, this, [this, childId] + { + emit showPreviousResults( childId ); + } ); addCommentItemForComponent( model, it.value(), item ); } diff --git a/src/gui/processing/models/qgsmodelgraphicsscene.h b/src/gui/processing/models/qgsmodelgraphicsscene.h index b37f5fe0b2c7..ed31e46469c5 100644 --- a/src/gui/processing/models/qgsmodelgraphicsscene.h +++ b/src/gui/processing/models/qgsmodelgraphicsscene.h @@ -192,6 +192,13 @@ class GUI_EXPORT QgsModelGraphicsScene : public QGraphicsScene */ void selectedItemChanged( QgsModelComponentGraphicItem *selected ); + /** + * Emitted when the user opts to view previous results from the child algorithm with matching ID. + * + * \since QGIS 3.38 + */ + void showPreviousResults( const QString &childId ); + protected: /**