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