QGIS API Documentation 3.43.0-Master (cd2944e57f9)
qgsmodeldesignerdialog.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsmodeldesignerdialog.cpp
3 ------------------------
4 Date : March 2020
5 Copyright : (C) 2020 Nyall Dawson
6 Email : nyall dot dawson at gmail dot com
7 ***************************************************************************
8 * *
9 * This program is free software; you can redistribute it and/or modify *
10 * it under the terms of the GNU General Public License as published by *
11 * the Free Software Foundation; either version 2 of the License, or *
12 * (at your option) any later version. *
13 * *
14 ***************************************************************************/
15
17#include "moc_qgsmodeldesignerdialog.cpp"
18#include "qgssettings.h"
19#include "qgsapplication.h"
20#include "qgsfileutils.h"
21#include "qgsmessagebar.h"
25#include "qgsgui.h"
27#include "qgsmodelundocommand.h"
29#include "qgsmodelviewtoolpan.h"
35#include "qgsmessageviewer.h"
36#include "qgsmessagebaritem.h"
37#include "qgspanelwidget.h"
40#include "qgsscreenhelper.h"
41#include "qgsmessagelog.h"
43#include "qgsproject.h"
44#include <QShortcut>
45#include <QKeySequence>
46#include <QFileDialog>
47#include <QPdfWriter>
48#include <QSvgGenerator>
49#include <QToolButton>
50#include <QCloseEvent>
51#include <QMessageBox>
52#include <QUndoView>
53#include <QPushButton>
54#include <QUrl>
55#include <QTextStream>
56#include <QActionGroup>
57
59
60
61QgsModelerToolboxModel::QgsModelerToolboxModel( QObject *parent )
63{
64}
65
66Qt::ItemFlags QgsModelerToolboxModel::flags( const QModelIndex &index ) const
67{
68 Qt::ItemFlags f = QgsProcessingToolboxProxyModel::flags( index );
69 const QModelIndex sourceIndex = mapToSource( index );
70 if ( toolboxModel()->isAlgorithm( sourceIndex ) || toolboxModel()->isParameter( sourceIndex ) )
71 {
72 f = f | Qt::ItemIsDragEnabled;
73 }
74 return f;
75}
76
77Qt::DropActions QgsModelerToolboxModel::supportedDragActions() const
78{
79 return Qt::CopyAction;
80}
81
82QgsModelDesignerDialog::QgsModelDesignerDialog( QWidget *parent, Qt::WindowFlags flags )
83 : QMainWindow( parent, flags )
84 , mToolsActionGroup( new QActionGroup( this ) )
85{
86 setupUi( this );
87
88 mLayerStore.setProject( QgsProject::instance() );
89
90 mScreenHelper = new QgsScreenHelper( this );
91
92 setAttribute( Qt::WA_DeleteOnClose );
93 setDockOptions( dockOptions() | QMainWindow::GroupedDragging );
94 setWindowFlags( Qt::WindowMinimizeButtonHint | Qt::WindowMaximizeButtonHint | Qt::WindowCloseButtonHint );
95
97
98 mModel = std::make_unique<QgsProcessingModelAlgorithm>();
99 mModel->setProvider( QgsApplication::processingRegistry()->providerById( QStringLiteral( "model" ) ) );
100
101 mUndoStack = new QUndoStack( this );
102 connect( mUndoStack, &QUndoStack::indexChanged, this, [=] {
103 if ( mIgnoreUndoStackChanges )
104 return;
105
106 mBlockUndoCommands++;
107 updateVariablesGui();
108 mGroupEdit->setText( mModel->group() );
109 mNameEdit->setText( mModel->displayName() );
110 mBlockUndoCommands--;
111 repaintModel();
112 } );
113
114 mPropertiesDock->setFeatures( QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetMovable );
115 mInputsDock->setFeatures( QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetMovable );
116 mAlgorithmsDock->setFeatures( QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetMovable );
117 mVariablesDock->setFeatures( QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetClosable );
118
119 mToolboxTree->header()->setVisible( false );
120 mToolboxSearchEdit->setShowSearchIcon( true );
121 mToolboxSearchEdit->setPlaceholderText( tr( "Search…" ) );
122 connect( mToolboxSearchEdit, &QgsFilterLineEdit::textChanged, mToolboxTree, &QgsProcessingToolboxTreeView::setFilterString );
123
124 mInputsTreeWidget->header()->setVisible( false );
125 mInputsTreeWidget->setAlternatingRowColors( true );
126 mInputsTreeWidget->setDragDropMode( QTreeWidget::DragOnly );
127 mInputsTreeWidget->setDropIndicatorShown( true );
128
129 mNameEdit->setPlaceholderText( tr( "Enter model name here" ) );
130 mGroupEdit->setPlaceholderText( tr( "Enter group name here" ) );
131
132 mMessageBar = new QgsMessageBar();
133 mMessageBar->setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Fixed );
134 mainLayout->insertWidget( 0, mMessageBar );
135
136 mView->setAcceptDrops( true );
137 QgsSettings settings;
138
139 connect( mActionClose, &QAction::triggered, this, &QWidget::close );
140 connect( mActionNew, &QAction::triggered, this, &QgsModelDesignerDialog::newModel );
141 connect( mActionZoomIn, &QAction::triggered, this, &QgsModelDesignerDialog::zoomIn );
142 connect( mActionZoomOut, &QAction::triggered, this, &QgsModelDesignerDialog::zoomOut );
143 connect( mActionZoomActual, &QAction::triggered, this, &QgsModelDesignerDialog::zoomActual );
144 connect( mActionZoomToItems, &QAction::triggered, this, &QgsModelDesignerDialog::zoomFull );
145 connect( mActionExportImage, &QAction::triggered, this, &QgsModelDesignerDialog::exportToImage );
146 connect( mActionExportPdf, &QAction::triggered, this, &QgsModelDesignerDialog::exportToPdf );
147 connect( mActionExportSvg, &QAction::triggered, this, &QgsModelDesignerDialog::exportToSvg );
148 connect( mActionExportPython, &QAction::triggered, this, &QgsModelDesignerDialog::exportAsPython );
149 connect( mActionSave, &QAction::triggered, this, [=] { saveModel( false ); } );
150 connect( mActionSaveAs, &QAction::triggered, this, [=] { saveModel( true ); } );
151 connect( mActionDeleteComponents, &QAction::triggered, this, &QgsModelDesignerDialog::deleteSelected );
152 connect( mActionSnapSelected, &QAction::triggered, mView, &QgsModelGraphicsView::snapSelected );
153 connect( mActionValidate, &QAction::triggered, this, &QgsModelDesignerDialog::validate );
154 connect( mActionReorderInputs, &QAction::triggered, this, &QgsModelDesignerDialog::reorderInputs );
155 connect( mActionReorderOutputs, &QAction::triggered, this, &QgsModelDesignerDialog::reorderOutputs );
156 connect( mActionEditHelp, &QAction::triggered, this, &QgsModelDesignerDialog::editHelp );
157 connect( mReorderInputsButton, &QPushButton::clicked, this, &QgsModelDesignerDialog::reorderInputs );
158 connect( mActionRun, &QAction::triggered, this, [this] { run(); } );
159 connect( mActionRunSelectedSteps, &QAction::triggered, this, &QgsModelDesignerDialog::runSelectedSteps );
160
161 mActionSnappingEnabled->setChecked( settings.value( QStringLiteral( "/Processing/Modeler/enableSnapToGrid" ), false ).toBool() );
162 connect( mActionSnappingEnabled, &QAction::toggled, this, [=]( bool enabled ) {
163 mView->snapper()->setSnapToGrid( enabled );
164 QgsSettings().setValue( QStringLiteral( "/Processing/Modeler/enableSnapToGrid" ), enabled );
165 } );
166 mView->snapper()->setSnapToGrid( mActionSnappingEnabled->isChecked() );
167
168 connect( mActionSelectAll, &QAction::triggered, this, [=] {
169 mScene->selectAll();
170 } );
171
172 QStringList docksTitle = settings.value( QStringLiteral( "ModelDesigner/hiddenDocksTitle" ), QStringList(), QgsSettings::App ).toStringList();
173 QStringList docksActive = settings.value( QStringLiteral( "ModelDesigner/hiddenDocksActive" ), QStringList(), QgsSettings::App ).toStringList();
174 if ( !docksTitle.isEmpty() )
175 {
176 for ( const auto &title : docksTitle )
177 {
178 mPanelStatus.insert( title, PanelStatus( true, docksActive.contains( title ) ) );
179 }
180 }
181 mActionHidePanels->setChecked( !docksTitle.isEmpty() );
182 connect( mActionHidePanels, &QAction::toggled, this, &QgsModelDesignerDialog::setPanelVisibility );
183
184 mUndoAction = mUndoStack->createUndoAction( this );
185 mUndoAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionUndo.svg" ) ) );
186 mUndoAction->setShortcuts( QKeySequence::Undo );
187 mRedoAction = mUndoStack->createRedoAction( this );
188 mRedoAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionRedo.svg" ) ) );
189 mRedoAction->setShortcuts( QKeySequence::Redo );
190
191 mMenuEdit->insertAction( mActionDeleteComponents, mRedoAction );
192 mMenuEdit->insertAction( mActionDeleteComponents, mUndoAction );
193 mMenuEdit->insertSeparator( mActionDeleteComponents );
194 mToolbar->insertAction( mActionZoomIn, mUndoAction );
195 mToolbar->insertAction( mActionZoomIn, mRedoAction );
196 mToolbar->insertSeparator( mActionZoomIn );
197
198 mGroupMenu = new QMenu( tr( "Zoom To" ), this );
199 mMenuView->insertMenu( mActionZoomIn, mGroupMenu );
200 connect( mGroupMenu, &QMenu::aboutToShow, this, &QgsModelDesignerDialog::populateZoomToMenu );
201
202 //cut/copy/paste actions. Note these are not included in the ui file
203 //as ui files have no support for QKeySequence shortcuts
204 mActionCut = new QAction( tr( "Cu&t" ), this );
205 mActionCut->setShortcuts( QKeySequence::Cut );
206 mActionCut->setStatusTip( tr( "Cut" ) );
207 mActionCut->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionEditCut.svg" ) ) );
208 connect( mActionCut, &QAction::triggered, this, [=] {
209 mView->copySelectedItems( QgsModelGraphicsView::ClipboardCut );
210 } );
211
212 mActionCopy = new QAction( tr( "&Copy" ), this );
213 mActionCopy->setShortcuts( QKeySequence::Copy );
214 mActionCopy->setStatusTip( tr( "Copy" ) );
215 mActionCopy->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionEditCopy.svg" ) ) );
216 connect( mActionCopy, &QAction::triggered, this, [=] {
217 mView->copySelectedItems( QgsModelGraphicsView::ClipboardCopy );
218 } );
219
220 mActionPaste = new QAction( tr( "&Paste" ), this );
221 mActionPaste->setShortcuts( QKeySequence::Paste );
222 mActionPaste->setStatusTip( tr( "Paste" ) );
223 mActionPaste->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionEditPaste.svg" ) ) );
224 connect( mActionPaste, &QAction::triggered, this, [=] {
225 mView->pasteItems( QgsModelGraphicsView::PasteModeCursor );
226 } );
227 mMenuEdit->insertAction( mActionDeleteComponents, mActionCut );
228 mMenuEdit->insertAction( mActionDeleteComponents, mActionCopy );
229 mMenuEdit->insertAction( mActionDeleteComponents, mActionPaste );
230 mMenuEdit->insertSeparator( mActionDeleteComponents );
231
232 mAlgorithmsModel = new QgsModelerToolboxModel( this );
233 mToolboxTree->setToolboxProxyModel( mAlgorithmsModel );
234
236 if ( settings.value( QStringLiteral( "Processing/Configuration/SHOW_ALGORITHMS_KNOWN_ISSUES" ), false ).toBool() )
237 {
239 }
240 mToolboxTree->setFilters( filters );
241 mToolboxTree->setDragDropMode( QTreeWidget::DragOnly );
242 mToolboxTree->setDropIndicatorShown( true );
243
244 connect( mView, &QgsModelGraphicsView::algorithmDropped, this, [=]( const QString &algorithmId, const QPointF &pos ) {
245 addAlgorithm( algorithmId, pos );
246 } );
247 connect( mView, &QgsModelGraphicsView::inputDropped, this, &QgsModelDesignerDialog::addInput );
248
249 connect( mToolboxTree, &QgsProcessingToolboxTreeView::doubleClicked, this, [=]( const QModelIndex & ) {
250 if ( mToolboxTree->selectedAlgorithm() )
251 addAlgorithm( mToolboxTree->selectedAlgorithm()->id(), QPointF() );
252 if ( mToolboxTree->selectedParameterType() )
253 addInput( mToolboxTree->selectedParameterType()->id(), QPointF() );
254 } );
255
256 connect( mInputsTreeWidget, &QgsModelDesignerInputsTreeWidget::doubleClicked, this, [=]( const QModelIndex & ) {
257 const QString parameterType = mInputsTreeWidget->currentItem()->data( 0, Qt::UserRole ).toString();
258 addInput( parameterType, QPointF() );
259 } );
260
261 // Ctrl+= should also trigger a zoom in action
262 QShortcut *ctrlEquals = new QShortcut( QKeySequence( QStringLiteral( "Ctrl+=" ) ), this );
263 connect( ctrlEquals, &QShortcut::activated, this, &QgsModelDesignerDialog::zoomIn );
264
265 mUndoDock = new QgsDockWidget( tr( "Undo History" ), this );
266 mUndoDock->setObjectName( QStringLiteral( "UndoDock" ) );
267 mUndoView = new QUndoView( mUndoStack, this );
268 mUndoDock->setWidget( mUndoView );
269 mUndoDock->setFeatures( QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetClosable );
270 addDockWidget( Qt::DockWidgetArea::LeftDockWidgetArea, mUndoDock );
271
272 tabifyDockWidget( mUndoDock, mPropertiesDock );
273 tabifyDockWidget( mVariablesDock, mPropertiesDock );
274 mPropertiesDock->raise();
275 tabifyDockWidget( mInputsDock, mAlgorithmsDock );
276 mInputsDock->raise();
277
278 connect( mVariablesEditor, &QgsVariableEditorWidget::scopeChanged, this, [=] {
279 if ( mModel )
280 {
281 beginUndoCommand( tr( "Change Model Variables" ) );
282 mModel->setVariables( mVariablesEditor->variablesInActiveScope() );
283 endUndoCommand();
284 }
285 } );
286 connect( mNameEdit, &QLineEdit::textChanged, this, [=]( const QString &name ) {
287 if ( mModel )
288 {
289 beginUndoCommand( tr( "Change Model Name" ), NameChanged );
290 mModel->setName( name );
291 endUndoCommand();
292 updateWindowTitle();
293 }
294 } );
295 connect( mGroupEdit, &QLineEdit::textChanged, this, [=]( const QString &group ) {
296 if ( mModel )
297 {
298 beginUndoCommand( tr( "Change Model Group" ), GroupChanged );
299 mModel->setGroup( group );
300 endUndoCommand();
301 updateWindowTitle();
302 }
303 } );
304
305 fillInputsTree();
306
307 QToolButton *toolbuttonExportToScript = new QToolButton();
308 toolbuttonExportToScript->setPopupMode( QToolButton::InstantPopup );
309 toolbuttonExportToScript->addAction( mActionExportAsScriptAlgorithm );
310 toolbuttonExportToScript->setDefaultAction( mActionExportAsScriptAlgorithm );
311 mToolbar->insertWidget( mActionExportImage, toolbuttonExportToScript );
312 connect( mActionExportAsScriptAlgorithm, &QAction::triggered, this, &QgsModelDesignerDialog::exportAsScriptAlgorithm );
313
314 mActionShowComments->setChecked( settings.value( QStringLiteral( "/Processing/Modeler/ShowComments" ), true ).toBool() );
315 connect( mActionShowComments, &QAction::toggled, this, &QgsModelDesignerDialog::toggleComments );
316
317 mPanTool = new QgsModelViewToolPan( mView );
318 mPanTool->setAction( mActionPan );
319
320 mToolsActionGroup->addAction( mActionPan );
321 connect( mActionPan, &QAction::triggered, mPanTool, [=] { mView->setTool( mPanTool ); } );
322
323 mSelectTool = new QgsModelViewToolSelect( mView );
324 mSelectTool->setAction( mActionSelectMoveItem );
325
326 mToolsActionGroup->addAction( mActionSelectMoveItem );
327 connect( mActionSelectMoveItem, &QAction::triggered, mSelectTool, [=] { mView->setTool( mSelectTool ); } );
328
329 mView->setTool( mSelectTool );
330 mView->setFocus();
331
332 connect( mView, &QgsModelGraphicsView::macroCommandStarted, this, [=]( const QString &text ) {
333 mIgnoreUndoStackChanges++;
334 mUndoStack->beginMacro( text );
335 mIgnoreUndoStackChanges--;
336 } );
337 connect( mView, &QgsModelGraphicsView::macroCommandEnded, this, [=] {
338 mIgnoreUndoStackChanges++;
339 mUndoStack->endMacro();
340 mIgnoreUndoStackChanges--;
341 } );
342 connect( mView, &QgsModelGraphicsView::commandBegun, this, [=]( const QString &text ) {
343 beginUndoCommand( text );
344 } );
345 connect( mView, &QgsModelGraphicsView::commandEnded, this, [=] {
346 endUndoCommand();
347 } );
348 connect( mView, &QgsModelGraphicsView::deleteSelectedItems, this, [=] {
349 deleteSelected();
350 } );
351
352 connect( mActionAddGroupBox, &QAction::triggered, this, [=] {
353 const QPointF viewCenter = mView->mapToScene( mView->viewport()->rect().center() );
354 QgsProcessingModelGroupBox group;
355 group.setPosition( viewCenter );
356 group.setDescription( tr( "New Group" ) );
357
358 beginUndoCommand( tr( "Add Group Box" ) );
359 model()->addGroupBox( group );
360 repaintModel();
361 endUndoCommand();
362 } );
363
364 updateWindowTitle();
365
366 // restore the toolbar and dock widgets positions using Qt settings API
367 restoreState( settings.value( QStringLiteral( "ModelDesigner/state" ), QByteArray(), QgsSettings::App ).toByteArray() );
368}
369
370QgsModelDesignerDialog::~QgsModelDesignerDialog()
371{
372 QgsSettings settings;
373 if ( !mPanelStatus.isEmpty() )
374 {
375 QStringList docksTitle;
376 QStringList docksActive;
377
378 for ( const auto &panel : mPanelStatus.toStdMap() )
379 {
380 if ( panel.second.isVisible )
381 docksTitle << panel.first;
382 if ( panel.second.isActive )
383 docksActive << panel.first;
384 }
385 settings.setValue( QStringLiteral( "ModelDesigner/hiddenDocksTitle" ), docksTitle, QgsSettings::App );
386 settings.setValue( QStringLiteral( "ModelDesigner/hiddenDocksActive" ), docksActive, QgsSettings::App );
387 }
388 else
389 {
390 settings.remove( QStringLiteral( "ModelDesigner/hiddenDocksTitle" ), QgsSettings::App );
391 settings.remove( QStringLiteral( "ModelDesigner/hiddenDocksActive" ), QgsSettings::App );
392 }
393
394 // store the toolbar/dock widget settings using Qt settings API
395 settings.setValue( QStringLiteral( "ModelDesigner/state" ), saveState(), QgsSettings::App );
396
397 mIgnoreUndoStackChanges++;
398 delete mSelectTool; // delete mouse handles before everything else
399}
400
401void QgsModelDesignerDialog::closeEvent( QCloseEvent *event )
402{
403 if ( checkForUnsavedChanges() )
404 event->accept();
405 else
406 event->ignore();
407}
408
409void QgsModelDesignerDialog::beginUndoCommand( const QString &text, int id )
410{
411 if ( mBlockUndoCommands || !mUndoStack )
412 return;
413
414 if ( mActiveCommand )
415 endUndoCommand();
416
417 mActiveCommand = std::make_unique<QgsModelUndoCommand>( mModel.get(), text, id );
418}
419
420void QgsModelDesignerDialog::endUndoCommand()
421{
422 if ( mBlockUndoCommands || !mActiveCommand || !mUndoStack )
423 return;
424
425 mActiveCommand->saveAfterState();
426 mIgnoreUndoStackChanges++;
427 mUndoStack->push( mActiveCommand.release() );
428 mIgnoreUndoStackChanges--;
429 setDirty( true );
430}
431
432QgsProcessingModelAlgorithm *QgsModelDesignerDialog::model()
433{
434 return mModel.get();
435}
436
437void QgsModelDesignerDialog::setModel( QgsProcessingModelAlgorithm *model )
438{
439 mModel.reset( model );
440
441 mGroupEdit->setText( mModel->group() );
442 mNameEdit->setText( mModel->displayName() );
443 repaintModel();
444 updateVariablesGui();
445
446 mView->centerOn( 0, 0 );
447 setDirty( false );
448
449 mIgnoreUndoStackChanges++;
450 mUndoStack->clear();
451 mIgnoreUndoStackChanges--;
452
453 updateWindowTitle();
454}
455
456void QgsModelDesignerDialog::loadModel( const QString &path )
457{
458 auto alg = std::make_unique<QgsProcessingModelAlgorithm>();
459 if ( alg->fromFile( path ) )
460 {
461 alg->setProvider( QgsApplication::processingRegistry()->providerById( QStringLiteral( "model" ) ) );
462 alg->setSourceFilePath( path );
463 setModel( alg.release() );
464 }
465 else
466 {
467 QgsMessageLog::logMessage( tr( "Could not load model %1" ).arg( path ), tr( "Processing" ), Qgis::MessageLevel::Critical );
468 QMessageBox::critical( this, tr( "Open Model" ), tr( "The selected model could not be loaded.\n"
469 "See the log for more information." ) );
470 }
471}
472
473void QgsModelDesignerDialog::setModelScene( QgsModelGraphicsScene *scene )
474{
475 QgsModelGraphicsScene *oldScene = mScene;
476
477 mScene = scene;
478 mScene->setParent( this );
479 mScene->setLastRunResult( mLastResult );
480 mScene->setModel( mModel.get() );
481 mScene->setMessageBar( mMessageBar );
482
483 const QPointF center = mView->mapToScene( mView->viewport()->rect().center() );
484 mView->setModelScene( mScene );
485
486 mSelectTool->resetCache();
487 mSelectTool->setScene( mScene );
488
489 connect( mScene, &QgsModelGraphicsScene::rebuildRequired, this, [=] {
490 if ( mBlockRepaints )
491 return;
492
493 repaintModel();
494 } );
495 connect( mScene, &QgsModelGraphicsScene::componentAboutToChange, this, [=]( const QString &description, int id ) { beginUndoCommand( description, id ); } );
496 connect( mScene, &QgsModelGraphicsScene::componentChanged, this, [=] { endUndoCommand(); } );
497 connect( mScene, &QgsModelGraphicsScene::runFromChild, this, &QgsModelDesignerDialog::runFromChild );
498 connect( mScene, &QgsModelGraphicsScene::runSelected, this, &QgsModelDesignerDialog::runSelectedSteps );
499 connect( mScene, &QgsModelGraphicsScene::showChildAlgorithmOutputs, this, &QgsModelDesignerDialog::showChildAlgorithmOutputs );
500 connect( mScene, &QgsModelGraphicsScene::showChildAlgorithmLog, this, &QgsModelDesignerDialog::showChildAlgorithmLog );
501
502 mView->centerOn( center );
503
504 if ( oldScene )
505 oldScene->deleteLater();
506}
507
508void QgsModelDesignerDialog::activate()
509{
510 show();
511 raise();
512 setWindowState( windowState() & ~Qt::WindowMinimized );
513 activateWindow();
514}
515
516void QgsModelDesignerDialog::updateVariablesGui()
517{
518 mBlockUndoCommands++;
519
520 auto variablesScope = std::make_unique<QgsExpressionContextScope>( tr( "Model Variables" ) );
521 const QVariantMap modelVars = mModel->variables();
522 for ( auto it = modelVars.constBegin(); it != modelVars.constEnd(); ++it )
523 {
524 variablesScope->setVariable( it.key(), it.value() );
525 }
526 QgsExpressionContext variablesContext;
527 variablesContext.appendScope( variablesScope.release() );
528 mVariablesEditor->setContext( &variablesContext );
529 mVariablesEditor->setEditableScopeIndex( 0 );
530
531 mBlockUndoCommands--;
532}
533
534void QgsModelDesignerDialog::setDirty( bool dirty )
535{
536 mHasChanged = dirty;
537 updateWindowTitle();
538}
539
540bool QgsModelDesignerDialog::validateSave( SaveAction action )
541{
542 switch ( action )
543 {
544 case QgsModelDesignerDialog::SaveAction::SaveAsFile:
545 break;
546 case QgsModelDesignerDialog::SaveAction::SaveInProject:
547 if ( mNameEdit->text().trimmed().isEmpty() )
548 {
549 mMessageBar->pushWarning( QString(), tr( "Please enter a model name before saving" ) );
550 return false;
551 }
552 break;
553 }
554
555 return true;
556}
557
558bool QgsModelDesignerDialog::checkForUnsavedChanges()
559{
560 if ( isDirty() )
561 {
562 QMessageBox::StandardButton ret = QMessageBox::question( this, tr( "Save Model?" ), tr( "There are unsaved changes in this model. Do you want to keep those?" ), QMessageBox::Save | QMessageBox::Cancel | QMessageBox::Discard, QMessageBox::Cancel );
563 switch ( ret )
564 {
565 case QMessageBox::Save:
566 return saveModel( false );
567
568 case QMessageBox::Discard:
569 return true;
570
571 default:
572 return false;
573 }
574 }
575 else
576 {
577 return true;
578 }
579}
580
581void QgsModelDesignerDialog::setLastRunResult( const QgsProcessingModelResult &result )
582{
583 mLastResult.mergeWith( result );
584 if ( mScene )
585 mScene->setLastRunResult( mLastResult );
586}
587
588void QgsModelDesignerDialog::setModelName( const QString &name )
589{
590 mNameEdit->setText( name );
591}
592
593void QgsModelDesignerDialog::zoomIn()
594{
595 mView->setTransformationAnchor( QGraphicsView::NoAnchor );
596 QPointF point = mView->mapToScene( QPoint( mView->viewport()->width() / 2.0, mView->viewport()->height() / 2 ) );
597 QgsSettings settings;
598 const double factor = settings.value( QStringLiteral( "/qgis/zoom_favor" ), 2.0 ).toDouble();
599 mView->scale( factor, factor );
600 mView->centerOn( point );
601}
602
603void QgsModelDesignerDialog::zoomOut()
604{
605 mView->setTransformationAnchor( QGraphicsView::NoAnchor );
606 QPointF point = mView->mapToScene( QPoint( mView->viewport()->width() / 2.0, mView->viewport()->height() / 2 ) );
607 QgsSettings settings;
608 const double factor = 1.0 / settings.value( QStringLiteral( "/qgis/zoom_favor" ), 2.0 ).toDouble();
609 mView->scale( factor, factor );
610 mView->centerOn( point );
611}
612
613void QgsModelDesignerDialog::zoomActual()
614{
615 QPointF point = mView->mapToScene( QPoint( mView->viewport()->width() / 2.0, mView->viewport()->height() / 2 ) );
616 mView->resetTransform();
617 mView->scale( mScreenHelper->screenDpi() / 96, mScreenHelper->screenDpi() / 96 );
618 mView->centerOn( point );
619}
620
621void QgsModelDesignerDialog::zoomFull()
622{
623 QRectF totalRect = mView->scene()->itemsBoundingRect();
624 totalRect.adjust( -10, -10, 10, 10 );
625 mView->fitInView( totalRect, Qt::KeepAspectRatio );
626}
627
628void QgsModelDesignerDialog::newModel()
629{
630 if ( !checkForUnsavedChanges() )
631 return;
632
633 auto alg = std::make_unique<QgsProcessingModelAlgorithm>();
634 alg->setProvider( QgsApplication::processingRegistry()->providerById( QStringLiteral( "model" ) ) );
635 setModel( alg.release() );
636}
637
638void QgsModelDesignerDialog::exportToImage()
639{
640 QgsSettings settings;
641 QString lastExportDir = settings.value( QStringLiteral( "lastModelDesignerExportDir" ), QDir::homePath(), QgsSettings::App ).toString();
642
643 QString filename = QFileDialog::getSaveFileName( this, tr( "Save Model as Image" ), lastExportDir, tr( "PNG files (*.png *.PNG)" ) );
644 // return dialog focus on Mac
645 activateWindow();
646 raise();
647 if ( filename.isEmpty() )
648 return;
649
650 filename = QgsFileUtils::ensureFileNameHasExtension( filename, QStringList() << QStringLiteral( "png" ) );
651
652 const QFileInfo saveFileInfo( filename );
653 settings.setValue( QStringLiteral( "lastModelDesignerExportDir" ), saveFileInfo.absolutePath(), QgsSettings::App );
654
655 repaintModel( false );
656
657 QRectF totalRect = mView->scene()->itemsBoundingRect();
658 totalRect.adjust( -10, -10, 10, 10 );
659 const QRectF imageRect = QRectF( 0, 0, totalRect.width(), totalRect.height() );
660
661 QImage img( totalRect.width(), totalRect.height(), QImage::Format_ARGB32_Premultiplied );
662 img.fill( Qt::white );
663 QPainter painter;
664 painter.setRenderHint( QPainter::Antialiasing );
665 painter.begin( &img );
666 mView->scene()->render( &painter, imageRect, totalRect );
667 painter.end();
668
669 img.save( filename );
670
671 mMessageBar->pushMessage( QString(), tr( "Successfully exported model as image to <a href=\"%1\">%2</a>" ).arg( QUrl::fromLocalFile( filename ).toString(), QDir::toNativeSeparators( filename ) ), Qgis::MessageLevel::Success, 0 );
672 repaintModel( true );
673}
674
675void QgsModelDesignerDialog::exportToPdf()
676{
677 QgsSettings settings;
678 QString lastExportDir = settings.value( QStringLiteral( "lastModelDesignerExportDir" ), QDir::homePath(), QgsSettings::App ).toString();
679
680 QString filename = QFileDialog::getSaveFileName( this, tr( "Save Model as PDF" ), lastExportDir, tr( "PDF files (*.pdf *.PDF)" ) );
681 // return dialog focus on Mac
682 activateWindow();
683 raise();
684 if ( filename.isEmpty() )
685 return;
686
687 filename = QgsFileUtils::ensureFileNameHasExtension( filename, QStringList() << QStringLiteral( "pdf" ) );
688
689 const QFileInfo saveFileInfo( filename );
690 settings.setValue( QStringLiteral( "lastModelDesignerExportDir" ), saveFileInfo.absolutePath(), QgsSettings::App );
691
692 repaintModel( false );
693
694 QRectF totalRect = mView->scene()->itemsBoundingRect();
695 totalRect.adjust( -10, -10, 10, 10 );
696 const QRectF printerRect = QRectF( 0, 0, totalRect.width(), totalRect.height() );
697
698 QPdfWriter pdfWriter( filename );
699
700 const double scaleFactor = 96 / 25.4; // based on 96 dpi sizes
701
702 QPageLayout pageLayout( QPageSize( totalRect.size() / scaleFactor, QPageSize::Millimeter ), QPageLayout::Portrait, QMarginsF( 0, 0, 0, 0 ) );
703 pageLayout.setMode( QPageLayout::FullPageMode );
704 pdfWriter.setPageLayout( pageLayout );
705
706 QPainter painter( &pdfWriter );
707 mView->scene()->render( &painter, printerRect, totalRect );
708 painter.end();
709
710 mMessageBar->pushMessage( QString(), tr( "Successfully exported model as PDF to <a href=\"%1\">%2</a>" ).arg( QUrl::fromLocalFile( filename ).toString(), QDir::toNativeSeparators( filename ) ), Qgis::MessageLevel::Success, 0 );
711 repaintModel( true );
712}
713
714void QgsModelDesignerDialog::exportToSvg()
715{
716 QgsSettings settings;
717 QString lastExportDir = settings.value( QStringLiteral( "lastModelDesignerExportDir" ), QDir::homePath(), QgsSettings::App ).toString();
718
719 QString filename = QFileDialog::getSaveFileName( this, tr( "Save Model as SVG" ), lastExportDir, tr( "SVG files (*.svg *.SVG)" ) );
720 // return dialog focus on Mac
721 activateWindow();
722 raise();
723 if ( filename.isEmpty() )
724 return;
725
726 filename = QgsFileUtils::ensureFileNameHasExtension( filename, QStringList() << QStringLiteral( "svg" ) );
727
728 const QFileInfo saveFileInfo( filename );
729 settings.setValue( QStringLiteral( "lastModelDesignerExportDir" ), saveFileInfo.absolutePath(), QgsSettings::App );
730
731 repaintModel( false );
732
733 QRectF totalRect = mView->scene()->itemsBoundingRect();
734 totalRect.adjust( -10, -10, 10, 10 );
735 const QRectF svgRect = QRectF( 0, 0, totalRect.width(), totalRect.height() );
736
737 QSvgGenerator svg;
738 svg.setFileName( filename );
739 svg.setSize( QSize( totalRect.width(), totalRect.height() ) );
740 svg.setViewBox( svgRect );
741 svg.setTitle( mModel->displayName() );
742
743 QPainter painter( &svg );
744 mView->scene()->render( &painter, svgRect, totalRect );
745 painter.end();
746
747 mMessageBar->pushMessage( QString(), tr( "Successfully exported model as SVG to <a href=\"%1\">%2</a>" ).arg( QUrl::fromLocalFile( filename ).toString(), QDir::toNativeSeparators( filename ) ), Qgis::MessageLevel::Success, 0 );
748 repaintModel( true );
749}
750
751void QgsModelDesignerDialog::exportAsPython()
752{
753 QgsSettings settings;
754 QString lastExportDir = settings.value( QStringLiteral( "lastModelDesignerExportDir" ), QDir::homePath(), QgsSettings::App ).toString();
755
756 QString filename = QFileDialog::getSaveFileName( this, tr( "Save Model as Python Script" ), lastExportDir, tr( "Processing scripts (*.py *.PY)" ) );
757 // return dialog focus on Mac
758 activateWindow();
759 raise();
760 if ( filename.isEmpty() )
761 return;
762
763 filename = QgsFileUtils::ensureFileNameHasExtension( filename, QStringList() << QStringLiteral( "py" ) );
764
765 const QFileInfo saveFileInfo( filename );
766 settings.setValue( QStringLiteral( "lastModelDesignerExportDir" ), saveFileInfo.absolutePath(), QgsSettings::App );
767
768 const QString text = mModel->asPythonCode( QgsProcessing::PythonOutputType::PythonQgsProcessingAlgorithmSubclass, 4 ).join( '\n' );
769
770 QFile outFile( filename );
771 if ( !outFile.open( QIODevice::WriteOnly | QIODevice::Truncate ) )
772 {
773 return;
774 }
775 QTextStream fout( &outFile );
776 fout << text;
777 outFile.close();
778
779 mMessageBar->pushMessage( QString(), tr( "Successfully exported model as Python script to <a href=\"%1\">%2</a>" ).arg( QUrl::fromLocalFile( filename ).toString(), QDir::toNativeSeparators( filename ) ), Qgis::MessageLevel::Success, 0 );
780}
781
782void QgsModelDesignerDialog::toggleComments( bool show )
783{
784 QgsSettings().setValue( QStringLiteral( "/Processing/Modeler/ShowComments" ), show );
785
786 repaintModel( true );
787}
788
789void QgsModelDesignerDialog::updateWindowTitle()
790{
791 QString title = tr( "Model Designer" );
792 if ( !mModel->name().isEmpty() )
793 title = mModel->group().isEmpty()
794 ? QStringLiteral( "%1: %2" ).arg( title, mModel->name() )
795 : QStringLiteral( "%1: %2 - %3" ).arg( title, mModel->group(), mModel->name() );
796
797 if ( isDirty() )
798 title.prepend( '*' );
799
800 setWindowTitle( title );
801}
802
803void QgsModelDesignerDialog::deleteSelected()
804{
805 QList<QgsModelComponentGraphicItem *> items = mScene->selectedComponentItems();
806 if ( items.empty() )
807 return;
808
809 if ( items.size() == 1 )
810 {
811 items.at( 0 )->deleteComponent();
812 return;
813 }
814
815 std::sort( items.begin(), items.end(), []( QgsModelComponentGraphicItem *p1, QgsModelComponentGraphicItem *p2 ) {
816 // try to delete the easy stuff first, so comments, then outputs, as nothing will depend on these...
817 // NOLINTBEGIN(bugprone-branch-clone)
818
819 // 1. comments
820 if ( dynamic_cast<QgsModelCommentGraphicItem *>( p1 ) && dynamic_cast<QgsModelCommentGraphicItem *>( p2 ) )
821 return false;
822 else if ( dynamic_cast<QgsModelCommentGraphicItem *>( p1 ) )
823 return true;
824 else if ( dynamic_cast<QgsModelCommentGraphicItem *>( p2 ) )
825 return false;
826 // 2. group boxes
827 else if ( dynamic_cast<QgsModelGroupBoxGraphicItem *>( p1 ) && dynamic_cast<QgsModelGroupBoxGraphicItem *>( p2 ) )
828 return false;
829 else if ( dynamic_cast<QgsModelGroupBoxGraphicItem *>( p1 ) )
830 return true;
831 else if ( dynamic_cast<QgsModelGroupBoxGraphicItem *>( p2 ) )
832 return false;
833 // 3. outputs
834 else if ( dynamic_cast<QgsModelOutputGraphicItem *>( p1 ) && dynamic_cast<QgsModelOutputGraphicItem *>( p2 ) )
835 return false;
836 else if ( dynamic_cast<QgsModelOutputGraphicItem *>( p1 ) )
837 return true;
838 else if ( dynamic_cast<QgsModelOutputGraphicItem *>( p2 ) )
839 return false;
840 // 4. child algorithms
841 else if ( dynamic_cast<QgsModelChildAlgorithmGraphicItem *>( p1 ) && dynamic_cast<QgsModelChildAlgorithmGraphicItem *>( p2 ) )
842 return false;
843 else if ( dynamic_cast<QgsModelChildAlgorithmGraphicItem *>( p1 ) )
844 return true;
845 else if ( dynamic_cast<QgsModelChildAlgorithmGraphicItem *>( p2 ) )
846 return false;
847 return false;
848 // NOLINTEND(bugprone-branch-clone)
849 } );
850
851
852 beginUndoCommand( tr( "Delete Components" ) );
853
854 QVariant prevState = mModel->toVariant();
855 mBlockUndoCommands++;
856 mBlockRepaints = true;
857 bool failed = false;
858 while ( !items.empty() )
859 {
860 QgsModelComponentGraphicItem *toDelete = nullptr;
861 for ( QgsModelComponentGraphicItem *item : items )
862 {
863 if ( item->canDeleteComponent() )
864 {
865 toDelete = item;
866 break;
867 }
868 }
869
870 if ( !toDelete )
871 {
872 failed = true;
873 break;
874 }
875
876 toDelete->deleteComponent();
877 items.removeAll( toDelete );
878 }
879
880 if ( failed )
881 {
882 mModel->loadVariant( prevState );
883 QMessageBox::warning( nullptr, QObject::tr( "Could not remove components" ), QObject::tr( "Components depend on the selected items.\n"
884 "Try to remove them before trying deleting these components." ) );
885 mBlockUndoCommands--;
886 mActiveCommand.reset();
887 }
888 else
889 {
890 mBlockUndoCommands--;
891 endUndoCommand();
892 }
893
894 mBlockRepaints = false;
895 repaintModel();
896}
897
898void QgsModelDesignerDialog::populateZoomToMenu()
899{
900 mGroupMenu->clear();
901 for ( const QgsProcessingModelGroupBox &box : model()->groupBoxes() )
902 {
903 if ( QgsModelComponentGraphicItem *item = mScene->groupBoxItem( box.uuid() ) )
904 {
905 QAction *zoomAction = new QAction( box.description(), mGroupMenu );
906 connect( zoomAction, &QAction::triggered, this, [=] {
907 QRectF groupRect = item->mapToScene( item->boundingRect() ).boundingRect();
908 groupRect.adjust( -10, -10, 10, 10 );
909 mView->fitInView( groupRect, Qt::KeepAspectRatio );
910 mView->centerOn( item );
911 } );
912 mGroupMenu->addAction( zoomAction );
913 }
914 }
915}
916
917void QgsModelDesignerDialog::setPanelVisibility( bool hidden )
918{
919 const QList<QDockWidget *> docks = findChildren<QDockWidget *>();
920 const QList<QTabBar *> tabBars = findChildren<QTabBar *>();
921
922 if ( hidden )
923 {
924 mPanelStatus.clear();
925 //record status of all docks
926 for ( QDockWidget *dock : docks )
927 {
928 mPanelStatus.insert( dock->windowTitle(), PanelStatus( dock->isVisible(), false ) );
929 dock->setVisible( false );
930 }
931
932 //record active dock tabs
933 for ( QTabBar *tabBar : tabBars )
934 {
935 QString currentTabTitle = tabBar->tabText( tabBar->currentIndex() );
936 mPanelStatus[currentTabTitle].isActive = true;
937 }
938 }
939 else
940 {
941 //restore visibility of all docks
942 for ( QDockWidget *dock : docks )
943 {
944 if ( mPanelStatus.contains( dock->windowTitle() ) )
945 {
946 dock->setVisible( mPanelStatus.value( dock->windowTitle() ).isVisible );
947 }
948 }
949
950 //restore previously active dock tabs
951 for ( QTabBar *tabBar : tabBars )
952 {
953 //loop through all tabs in tab bar
954 for ( int i = 0; i < tabBar->count(); ++i )
955 {
956 QString tabTitle = tabBar->tabText( i );
957 if ( mPanelStatus.contains( tabTitle ) && mPanelStatus.value( tabTitle ).isActive )
958 {
959 tabBar->setCurrentIndex( i );
960 }
961 }
962 }
963 mPanelStatus.clear();
964 }
965}
966
967void QgsModelDesignerDialog::editHelp()
968{
969 QgsProcessingHelpEditorDialog dialog( this );
970 dialog.setWindowTitle( tr( "Edit Model Help" ) );
971 dialog.setAlgorithm( mModel.get() );
972 if ( dialog.exec() )
973 {
974 beginUndoCommand( tr( "Edit Model Help" ) );
975 mModel->setHelpContent( dialog.helpContent() );
976 endUndoCommand();
977 }
978}
979
980void QgsModelDesignerDialog::runSelectedSteps()
981{
982 QSet<QString> children;
983 const QList<QgsModelComponentGraphicItem *> items = mScene->selectedComponentItems();
984 for ( QgsModelComponentGraphicItem *item : items )
985 {
986 if ( QgsProcessingModelChildAlgorithm *childAlgorithm = dynamic_cast<QgsProcessingModelChildAlgorithm *>( item->component() ) )
987 {
988 children.insert( childAlgorithm->childId() );
989 }
990 }
991
992 if ( children.isEmpty() )
993 {
994 mMessageBar->pushWarning( QString(), tr( "No steps are selected" ) );
995 return;
996 }
997
998 run( children );
999}
1000
1001void QgsModelDesignerDialog::runFromChild( const QString &id )
1002{
1003 QSet<QString> children = mModel->dependentChildAlgorithms( id );
1004 children.insert( id );
1005 run( children );
1006}
1007
1008void QgsModelDesignerDialog::run( const QSet<QString> &childAlgorithmSubset )
1009{
1010 QStringList errors;
1011 const bool isValid = model()->validate( errors );
1012 if ( !isValid )
1013 {
1014 QMessageBox messageBox;
1015 messageBox.setWindowTitle( tr( "Model is Invalid" ) );
1016 messageBox.setIcon( QMessageBox::Icon::Warning );
1017 messageBox.setText( tr( "This model is not valid and contains one or more issues. Are you sure you want to run it in this state?" ) );
1018 messageBox.setStandardButtons( QMessageBox::StandardButton::Yes | QMessageBox::StandardButton::Cancel );
1019 messageBox.setDefaultButton( QMessageBox::StandardButton::Cancel );
1020
1021 QString errorString;
1022 for ( const QString &error : std::as_const( errors ) )
1023 {
1024 QString cleanedError = error;
1025 const thread_local QRegularExpression re( QStringLiteral( "<[^>]*>" ) );
1026 cleanedError.replace( re, QString() );
1027 errorString += QStringLiteral( "• %1\n" ).arg( cleanedError );
1028 }
1029
1030 messageBox.setDetailedText( errorString );
1031 if ( messageBox.exec() == QMessageBox::StandardButton::Cancel )
1032 return;
1033 }
1034
1035 if ( !childAlgorithmSubset.isEmpty() )
1036 {
1037 for ( const QString &child : childAlgorithmSubset )
1038 {
1039 // has user previously run all requirements for this step?
1040 const QSet<QString> requirements = mModel->dependsOnChildAlgorithms( child );
1041 for ( const QString &requirement : requirements )
1042 {
1043 if ( !mLastResult.executedChildIds().contains( requirement ) )
1044 {
1045 QMessageBox messageBox;
1046 messageBox.setWindowTitle( tr( "Run Model" ) );
1047 messageBox.setIcon( QMessageBox::Icon::Warning );
1048 messageBox.setText( tr( "Prerequisite parts of this model have not yet been run (try running the full model first)." ) );
1049 messageBox.setStandardButtons( QMessageBox::StandardButton::Ok );
1050 messageBox.exec();
1051 return;
1052 }
1053 }
1054 }
1055 }
1056
1057 std::unique_ptr<QgsProcessingAlgorithmDialogBase> dialog( createExecutionDialog() );
1058 if ( !dialog )
1059 return;
1060
1061 dialog->setLogLevel( Qgis::ProcessingLogLevel::ModelDebug );
1062 dialog->setParameters( mModel->designerParameterValues() );
1063
1064 connect( dialog.get(), &QgsProcessingAlgorithmDialogBase::algorithmAboutToRun, this, [this, &childAlgorithmSubset]( QgsProcessingContext *context ) {
1065 if ( !childAlgorithmSubset.empty() )
1066 {
1067 // start from previous state
1068 auto modelConfig = std::make_unique<QgsProcessingModelInitialRunConfig>();
1069 modelConfig->setChildAlgorithmSubset( childAlgorithmSubset );
1070 modelConfig->setPreviouslyExecutedChildAlgorithms( mLastResult.executedChildIds() );
1071 modelConfig->setInitialChildInputs( mLastResult.rawChildInputs() );
1072 modelConfig->setInitialChildOutputs( mLastResult.rawChildOutputs() );
1073
1074 // add copies of layers from previous runs to context's layer store, so that they can be used
1075 // when running the subset
1076 const QMap<QString, QgsMapLayer *> previousOutputLayers = mLayerStore.temporaryLayerStore()->mapLayers();
1077 auto previousResultStore = std::make_unique<QgsMapLayerStore>();
1078 for ( auto it = previousOutputLayers.constBegin(); it != previousOutputLayers.constEnd(); ++it )
1079 {
1080 std::unique_ptr<QgsMapLayer> clone( it.value()->clone() );
1081 clone->setId( it.value()->id() );
1082 previousResultStore->addMapLayer( clone.release() );
1083 }
1084 previousResultStore->moveToThread( nullptr );
1085 modelConfig->setPreviousLayerStore( std::move( previousResultStore ) );
1086 context->setModelInitialRunConfig( std::move( modelConfig ) );
1087 }
1088 } );
1089
1090 connect( dialog.get(), &QgsProcessingAlgorithmDialogBase::algorithmFinished, this, [this, &dialog]( bool, const QVariantMap & ) {
1091 QgsProcessingContext *context = dialog->processingContext();
1092
1093 setLastRunResult( context->modelResult() );
1094
1095 mModel->setDesignerParameterValues( dialog->createProcessingParameters( QgsProcessingParametersGenerator::Flag::SkipDefaultValueParameters ) );
1096
1097 // take child output layers
1098 mLayerStore.temporaryLayerStore()->removeAllMapLayers();
1099 mLayerStore.takeResultsFrom( *context );
1100 } );
1101
1102 dialog->exec();
1103}
1104
1105void QgsModelDesignerDialog::showChildAlgorithmOutputs( const QString &childId )
1106{
1107 const QString childDescription = mModel->childAlgorithm( childId ).description();
1108
1109 const QgsProcessingModelChildAlgorithmResult result = mLastResult.childResults().value( childId );
1110 const QVariantMap childAlgorithmOutputs = result.outputs();
1111 if ( childAlgorithmOutputs.isEmpty() )
1112 {
1113 mMessageBar->pushWarning( QString(), tr( "No results are available for %1" ).arg( childDescription ) );
1114 return;
1115 }
1116
1117 const QgsProcessingAlgorithm *algorithm = mModel->childAlgorithm( childId ).algorithm();
1118 if ( !algorithm )
1119 {
1120 mMessageBar->pushCritical( QString(), tr( "Results cannot be shown for an invalid model component" ) );
1121 return;
1122 }
1123
1124 const QList<const QgsProcessingParameterDefinition *> outputParams = algorithm->destinationParameterDefinitions();
1125 if ( outputParams.isEmpty() )
1126 {
1127 // this situation should not arise in normal use, we don't show the action in this case
1128 QgsDebugError( "Cannot show results for algorithms with no outputs" );
1129 return;
1130 }
1131
1132 bool foundResults = false;
1133 for ( const QgsProcessingParameterDefinition *outputParam : outputParams )
1134 {
1135 const QVariant output = childAlgorithmOutputs.value( outputParam->name() );
1136 if ( !output.isValid() )
1137 continue;
1138
1139 if ( output.type() == QVariant::String )
1140 {
1141 if ( QgsMapLayer *resultLayer = QgsProcessingUtils::mapLayerFromString( output.toString(), mLayerStore ) )
1142 {
1143 QgsDebugMsgLevel( QStringLiteral( "Loading previous result for %1: %2" ).arg( outputParam->name(), output.toString() ), 2 );
1144
1145 std::unique_ptr<QgsMapLayer> layer( resultLayer->clone() );
1146
1147 QString baseName;
1148 if ( outputParams.size() > 1 )
1149 baseName = tr( "%1 — %2" ).arg( childDescription, outputParam->name() );
1150 else
1151 baseName = childDescription;
1152
1153 // make name unique, so that's it's easy to see which is the most recent result.
1154 // (this helps when running the model multiple times.)
1155 QString name = baseName;
1156 int counter = 1;
1157 while ( !QgsProject::instance()->mapLayersByName( name ).empty() )
1158 {
1159 counter += 1;
1160 name = tr( "%1 (%2)" ).arg( baseName ).arg( counter );
1161 }
1162
1163 layer->setName( name );
1164
1165 QgsProject::instance()->addMapLayer( layer.release() );
1166 foundResults = true;
1167 }
1168 else
1169 {
1170 // should not happen in normal operation
1171 QgsDebugError( QStringLiteral( "Could not load previous result for %1: %2" ).arg( outputParam->name(), output.toString() ) );
1172 }
1173 }
1174 }
1175
1176 if ( !foundResults )
1177 {
1178 mMessageBar->pushWarning( QString(), tr( "No results are available for %1" ).arg( childDescription ) );
1179 return;
1180 }
1181}
1182
1183void QgsModelDesignerDialog::showChildAlgorithmLog( const QString &childId )
1184{
1185 const QString childDescription = mModel->childAlgorithm( childId ).description();
1186
1187 const QgsProcessingModelChildAlgorithmResult result = mLastResult.childResults().value( childId );
1188 if ( result.htmlLog().isEmpty() )
1189 {
1190 mMessageBar->pushWarning( QString(), tr( "No log is available for %1" ).arg( childDescription ) );
1191 return;
1192 }
1193
1194 QgsMessageViewer m( this, QgsGuiUtils::ModalDialogFlags, false );
1195 m.setWindowTitle( childDescription );
1196 m.setCheckBoxVisible( false );
1197 m.setMessageAsHtml( result.htmlLog() );
1198 m.exec();
1199}
1200
1201void QgsModelDesignerDialog::validate()
1202{
1203 QStringList issues;
1204 if ( model()->validate( issues ) )
1205 {
1206 mMessageBar->pushSuccess( QString(), tr( "Model is valid!" ) );
1207 }
1208 else
1209 {
1210 QgsMessageBarItem *messageWidget = QgsMessageBar::createMessage( QString(), tr( "Model is invalid!" ) );
1211 QPushButton *detailsButton = new QPushButton( tr( "Details" ) );
1212 connect( detailsButton, &QPushButton::clicked, detailsButton, [=] {
1213 QgsMessageViewer *dialog = new QgsMessageViewer( detailsButton );
1214 dialog->setTitle( tr( "Model is Invalid" ) );
1215
1216 QString longMessage = tr( "<p>This model is not valid:</p>" ) + QStringLiteral( "<ul>" );
1217 for ( const QString &issue : issues )
1218 {
1219 longMessage += QStringLiteral( "<li>%1</li>" ).arg( issue );
1220 }
1221 longMessage += QLatin1String( "</ul>" );
1222
1223 dialog->setMessage( longMessage, QgsMessageOutput::MessageHtml );
1224 dialog->showMessage();
1225 } );
1226 messageWidget->layout()->addWidget( detailsButton );
1227 mMessageBar->clearWidgets();
1228 mMessageBar->pushWidget( messageWidget, Qgis::MessageLevel::Warning, 0 );
1229 }
1230}
1231
1232void QgsModelDesignerDialog::reorderInputs()
1233{
1234 QgsModelInputReorderDialog dlg( this );
1235 dlg.setModel( mModel.get() );
1236 if ( dlg.exec() )
1237 {
1238 const QStringList inputOrder = dlg.inputOrder();
1239 beginUndoCommand( tr( "Reorder Inputs" ) );
1240 mModel->setParameterOrder( inputOrder );
1241 endUndoCommand();
1242 }
1243}
1244
1245void QgsModelDesignerDialog::reorderOutputs()
1246{
1247 QgsModelOutputReorderDialog dlg( this );
1248 dlg.setModel( mModel.get() );
1249 if ( dlg.exec() )
1250 {
1251 const QStringList outputOrder = dlg.outputOrder();
1252 beginUndoCommand( tr( "Reorder Outputs" ) );
1253 mModel->setOutputOrder( outputOrder );
1254 mModel->setOutputGroup( dlg.outputGroup() );
1255 endUndoCommand();
1256 }
1257}
1258
1259bool QgsModelDesignerDialog::isDirty() const
1260{
1261 return mHasChanged && mUndoStack->index() != -1;
1262}
1263
1264void QgsModelDesignerDialog::fillInputsTree()
1265{
1266 const QIcon icon = QgsApplication::getThemeIcon( QStringLiteral( "mIconModelInput.svg" ) );
1267 auto parametersItem = std::make_unique<QTreeWidgetItem>();
1268 parametersItem->setText( 0, tr( "Parameters" ) );
1269 QList<QgsProcessingParameterType *> available = QgsApplication::processingRegistry()->parameterTypes();
1270 std::sort( available.begin(), available.end(), []( const QgsProcessingParameterType *a, const QgsProcessingParameterType *b ) -> bool {
1271 return QString::localeAwareCompare( a->name(), b->name() ) < 0;
1272 } );
1273
1274 for ( QgsProcessingParameterType *param : std::as_const( available ) )
1275 {
1277 {
1278 auto paramItem = std::make_unique<QTreeWidgetItem>();
1279 paramItem->setText( 0, param->name() );
1280 paramItem->setData( 0, Qt::UserRole, param->id() );
1281 paramItem->setIcon( 0, icon );
1282 paramItem->setFlags( Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled );
1283 paramItem->setToolTip( 0, param->description() );
1284 parametersItem->addChild( paramItem.release() );
1285 }
1286 }
1287 mInputsTreeWidget->addTopLevelItem( parametersItem.release() );
1288 mInputsTreeWidget->topLevelItem( 0 )->setExpanded( true );
1289}
1290
1291
1292//
1293// QgsModelChildDependenciesWidget
1294//
1295
1296QgsModelChildDependenciesWidget::QgsModelChildDependenciesWidget( QWidget *parent, QgsProcessingModelAlgorithm *model, const QString &childId )
1297 : QWidget( parent )
1298 , mModel( model )
1299 , mChildId( childId )
1300{
1301 QHBoxLayout *hl = new QHBoxLayout();
1302 hl->setContentsMargins( 0, 0, 0, 0 );
1303
1304 mLineEdit = new QLineEdit();
1305 mLineEdit->setEnabled( false );
1306 hl->addWidget( mLineEdit, 1 );
1307
1308 mToolButton = new QToolButton();
1309 mToolButton->setText( QString( QChar( 0x2026 ) ) );
1310 hl->addWidget( mToolButton );
1311
1312 setLayout( hl );
1313
1314 mLineEdit->setText( tr( "%1 dependencies selected" ).arg( 0 ) );
1315
1316 connect( mToolButton, &QToolButton::clicked, this, &QgsModelChildDependenciesWidget::showDialog );
1317}
1318
1319void QgsModelChildDependenciesWidget::setValue( const QList<QgsProcessingModelChildDependency> &value )
1320{
1321 mValue = value;
1322
1323 updateSummaryText();
1324}
1325
1326void QgsModelChildDependenciesWidget::showDialog()
1327{
1328 const QList<QgsProcessingModelChildDependency> available = mModel->availableDependenciesForChildAlgorithm( mChildId );
1329
1330 QVariantList availableOptions;
1331 for ( const QgsProcessingModelChildDependency &dep : available )
1332 availableOptions << QVariant::fromValue( dep );
1333 QVariantList selectedOptions;
1334 for ( const QgsProcessingModelChildDependency &dep : mValue )
1335 selectedOptions << QVariant::fromValue( dep );
1336
1338 if ( panel )
1339 {
1340 QgsProcessingMultipleSelectionPanelWidget *widget = new QgsProcessingMultipleSelectionPanelWidget( availableOptions, selectedOptions );
1341 widget->setPanelTitle( tr( "Algorithm Dependencies" ) );
1342
1343 widget->setValueFormatter( [=]( const QVariant &v ) -> QString {
1344 const QgsProcessingModelChildDependency dep = v.value<QgsProcessingModelChildDependency>();
1345
1346 const QString description = mModel->childAlgorithm( dep.childId ).description();
1347 if ( dep.conditionalBranch.isEmpty() )
1348 return description;
1349 else
1350 return tr( "Condition “%1” from algorithm “%2”" ).arg( dep.conditionalBranch, description );
1351 } );
1352
1353 connect( widget, &QgsProcessingMultipleSelectionPanelWidget::selectionChanged, this, [=]() {
1354 QList<QgsProcessingModelChildDependency> res;
1355 for ( const QVariant &v : widget->selectedOptions() )
1356 {
1357 res << v.value<QgsProcessingModelChildDependency>();
1358 }
1359 setValue( res );
1360 } );
1361 connect( widget, &QgsProcessingMultipleSelectionPanelWidget::acceptClicked, widget, &QgsPanelWidget::acceptPanel );
1362 panel->openPanel( widget );
1363 }
1364}
1365
1366void QgsModelChildDependenciesWidget::updateSummaryText()
1367{
1368 mLineEdit->setText( tr( "%n dependencies selected", nullptr, mValue.count() ) );
1369}
1370
@ ExposeToModeler
Is this parameter available in the modeler. Is set to on by default.
@ Warning
Warning message.
Definition qgis.h:156
@ Critical
Critical/error message.
Definition qgis.h:157
@ Success
Used for reporting a successful operation.
Definition qgis.h:158
@ ModelDebug
Model debug level logging. Includes verbose logging and other outputs useful for debugging models.
static QgsProcessingRegistry * processingRegistry()
Returns the application's processing registry, used for managing processing providers,...
static QIcon getThemeIcon(const QString &name, const QColor &fillColor=QColor(), const QColor &strokeColor=QColor())
Helper to get a theme icon.
A QDockWidget subclass with more fine-grained control over how the widget is closed or opened.
Expression contexts are used to encapsulate the parameters around which a QgsExpression should be eva...
void appendScope(QgsExpressionContextScope *scope)
Appends a scope to the end of the context.
static QString ensureFileNameHasExtension(const QString &fileName, const QStringList &extensions)
Ensures that a fileName ends with an extension from the provided list of extensions.
static void enableAutoGeometryRestore(QWidget *widget, const QString &key=QString())
Register the widget to allow its position to be automatically saved and restored when open and closed...
Definition qgsgui.cpp:210
Base class for all map layer types.
Definition qgsmaplayer.h:77
Represents an item shown within a QgsMessageBar widget.
A bar for displaying non-blocking messages to the user.
static QgsMessageBarItem * createMessage(const QString &text, QWidget *parent=nullptr)
Creates message bar item widget containing a message text to be displayed on the bar.
static void logMessage(const QString &message, const QString &tag=QString(), Qgis::MessageLevel level=Qgis::MessageLevel::Warning, bool notifyUser=true, const char *file=__builtin_FILE(), const char *function=__builtin_FUNCTION(), int line=__builtin_LINE())
Adds a message to the log instance (and creates it if necessary).
A generic message view for displaying QGIS messages.
void setTitle(const QString &title) override
Sets title for the messages.
void setMessage(const QString &message, MessageType msgType) override
Sets message, it won't be displayed until.
void showMessage(bool blocking=true) override
display the message to the user and deletes itself
Model designer view tool for panning a model.
Model designer view tool for selecting items in the model.
Base class for any widget that can be shown as an inline panel.
void openPanel(QgsPanelWidget *panel)
Open a panel or dialog depending on dock mode setting If dock mode is true this method will emit the ...
void acceptPanel()
Accept the panel.
static QgsPanelWidget * findParentPanel(QWidget *widget)
Traces through the parents of a widget to find if it is contained within a QgsPanelWidget widget.
Abstract base class for processing algorithms.
QgsProcessingParameterDefinitions destinationParameterDefinitions() const
Returns a list of destination parameters definitions utilized by the algorithm.
Contains information about the context in which a processing algorithm is executed.
Encapsulates the results of running a child algorithm within a model.
QString htmlLog() const
Returns the HTML formatted contents of logged messages which occurred while running the child.
QVariantMap outputs() const
Returns the outputs generated by the child algorithm.
Encapsulates the results of running a Processing model.
QMap< QString, QgsProcessingModelChildAlgorithmResult > childResults() const
Returns the map of child algorithm results.
Base class for the definition of processing parameters.
Makes metadata of processing parameters available.
QList< QgsProcessingParameterType * > parameterTypes() const
Returns a list with all known parameter types.
A proxy model for providers and algorithms shown within the Processing toolbox.
@ ShowKnownIssues
Show algorithms with known issues (hidden by default)
@ Modeler
Filters out any algorithms and content which should not be shown in the modeler.
static QgsMapLayer * mapLayerFromString(const QString &string, QgsProcessingContext &context, bool allowLoadingNewLayers=true, QgsProcessingUtils::LayerHint typeHint=QgsProcessingUtils::LayerHint::UnknownType, QgsProcessing::LayerOptionsFlags flags=QgsProcessing::LayerOptionsFlags())
Interprets a string as a map layer within the supplied context.
@ PythonQgsProcessingAlgorithmSubclass
Full Python QgsProcessingAlgorithm subclass.
static QgsProject * instance()
Returns the QgsProject singleton instance.
QgsMapLayer * addMapLayer(QgsMapLayer *mapLayer, bool addToLegend=true, bool takeOwnership=true)
Add a layer to the map of loaded layers.
A utility class for dynamic handling of changes to screen properties.
Stores settings for use within QGIS.
Definition qgssettings.h:65
QVariant value(const QString &key, const QVariant &defaultValue=QVariant(), Section section=NoSection) const
Returns the value for setting key.
void remove(const QString &key, QgsSettings::Section section=QgsSettings::NoSection)
Removes the setting key and any sub-settings of key in a section.
void setValue(const QString &key, const QVariant &value, QgsSettings::Section section=QgsSettings::NoSection)
Sets the value of setting key to value.
void scopeChanged()
Emitted when the user has modified a scope using the widget.
As part of the API refactoring and improvements which landed in the Processing API was substantially reworked from the x version This was done in order to allow much of the underlying Processing framework to be ported into allowing algorithms to be written in pure substantial changes are required in order to port existing x Processing algorithms for QGIS x The most significant changes are outlined not GeoAlgorithm For algorithms which operate on features one by consider subclassing the QgsProcessingFeatureBasedAlgorithm class This class allows much of the boilerplate code for looping over features from a vector layer to be bypassed and instead requires implementation of a processFeature method Ensure that your algorithm(or algorithm 's parent class) implements the new pure virtual createInstance(self) call
#define QgsDebugMsgLevel(str, level)
Definition qgslogger.h:41
#define QgsDebugError(str)
Definition qgslogger.h:40