QGIS API Documentation 3.43.0-Master (a6cade80589)
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::beginCommand, this, [=]( const QString &text ) {
343 beginUndoCommand( text );
344 } );
345 connect( mView, &QgsModelGraphicsView::endCommand, 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 if ( dynamic_cast<QgsModelCommentGraphicItem *>( p1 ) )
818 return true;
819 else if ( dynamic_cast<QgsModelCommentGraphicItem *>( p2 ) )
820 return false;
821 else if ( dynamic_cast<QgsModelGroupBoxGraphicItem *>( p1 ) )
822 return true;
823 else if ( dynamic_cast<QgsModelGroupBoxGraphicItem *>( p2 ) )
824 return false;
825 else if ( dynamic_cast<QgsModelOutputGraphicItem *>( p1 ) )
826 return true;
827 else if ( dynamic_cast<QgsModelOutputGraphicItem *>( p2 ) )
828 return false;
829 else if ( dynamic_cast<QgsModelChildAlgorithmGraphicItem *>( p1 ) )
830 return true;
831 else if ( dynamic_cast<QgsModelChildAlgorithmGraphicItem *>( p2 ) )
832 return false;
833 return false;
834 } );
835
836
837 beginUndoCommand( tr( "Delete Components" ) );
838
839 QVariant prevState = mModel->toVariant();
840 mBlockUndoCommands++;
841 mBlockRepaints = true;
842 bool failed = false;
843 while ( !items.empty() )
844 {
845 QgsModelComponentGraphicItem *toDelete = nullptr;
846 for ( QgsModelComponentGraphicItem *item : items )
847 {
848 if ( item->canDeleteComponent() )
849 {
850 toDelete = item;
851 break;
852 }
853 }
854
855 if ( !toDelete )
856 {
857 failed = true;
858 break;
859 }
860
861 toDelete->deleteComponent();
862 items.removeAll( toDelete );
863 }
864
865 if ( failed )
866 {
867 mModel->loadVariant( prevState );
868 QMessageBox::warning( nullptr, QObject::tr( "Could not remove components" ), QObject::tr( "Components depend on the selected items.\n"
869 "Try to remove them before trying deleting these components." ) );
870 mBlockUndoCommands--;
871 mActiveCommand.reset();
872 }
873 else
874 {
875 mBlockUndoCommands--;
876 endUndoCommand();
877 }
878
879 mBlockRepaints = false;
880 repaintModel();
881}
882
883void QgsModelDesignerDialog::populateZoomToMenu()
884{
885 mGroupMenu->clear();
886 for ( const QgsProcessingModelGroupBox &box : model()->groupBoxes() )
887 {
888 if ( QgsModelComponentGraphicItem *item = mScene->groupBoxItem( box.uuid() ) )
889 {
890 QAction *zoomAction = new QAction( box.description(), mGroupMenu );
891 connect( zoomAction, &QAction::triggered, this, [=] {
892 QRectF groupRect = item->mapToScene( item->boundingRect() ).boundingRect();
893 groupRect.adjust( -10, -10, 10, 10 );
894 mView->fitInView( groupRect, Qt::KeepAspectRatio );
895 mView->centerOn( item );
896 } );
897 mGroupMenu->addAction( zoomAction );
898 }
899 }
900}
901
902void QgsModelDesignerDialog::setPanelVisibility( bool hidden )
903{
904 const QList<QDockWidget *> docks = findChildren<QDockWidget *>();
905 const QList<QTabBar *> tabBars = findChildren<QTabBar *>();
906
907 if ( hidden )
908 {
909 mPanelStatus.clear();
910 //record status of all docks
911 for ( QDockWidget *dock : docks )
912 {
913 mPanelStatus.insert( dock->windowTitle(), PanelStatus( dock->isVisible(), false ) );
914 dock->setVisible( false );
915 }
916
917 //record active dock tabs
918 for ( QTabBar *tabBar : tabBars )
919 {
920 QString currentTabTitle = tabBar->tabText( tabBar->currentIndex() );
921 mPanelStatus[currentTabTitle].isActive = true;
922 }
923 }
924 else
925 {
926 //restore visibility of all docks
927 for ( QDockWidget *dock : docks )
928 {
929 if ( mPanelStatus.contains( dock->windowTitle() ) )
930 {
931 dock->setVisible( mPanelStatus.value( dock->windowTitle() ).isVisible );
932 }
933 }
934
935 //restore previously active dock tabs
936 for ( QTabBar *tabBar : tabBars )
937 {
938 //loop through all tabs in tab bar
939 for ( int i = 0; i < tabBar->count(); ++i )
940 {
941 QString tabTitle = tabBar->tabText( i );
942 if ( mPanelStatus.contains( tabTitle ) && mPanelStatus.value( tabTitle ).isActive )
943 {
944 tabBar->setCurrentIndex( i );
945 }
946 }
947 }
948 mPanelStatus.clear();
949 }
950}
951
952void QgsModelDesignerDialog::editHelp()
953{
954 QgsProcessingHelpEditorDialog dialog( this );
955 dialog.setWindowTitle( tr( "Edit Model Help" ) );
956 dialog.setAlgorithm( mModel.get() );
957 if ( dialog.exec() )
958 {
959 beginUndoCommand( tr( "Edit Model Help" ) );
960 mModel->setHelpContent( dialog.helpContent() );
961 endUndoCommand();
962 }
963}
964
965void QgsModelDesignerDialog::runSelectedSteps()
966{
967 QSet<QString> children;
968 const QList<QgsModelComponentGraphicItem *> items = mScene->selectedComponentItems();
969 for ( QgsModelComponentGraphicItem *item : items )
970 {
971 if ( QgsProcessingModelChildAlgorithm *childAlgorithm = dynamic_cast<QgsProcessingModelChildAlgorithm *>( item->component() ) )
972 {
973 children.insert( childAlgorithm->childId() );
974 }
975 }
976
977 if ( children.isEmpty() )
978 {
979 mMessageBar->pushWarning( QString(), tr( "No steps are selected" ) );
980 return;
981 }
982
983 run( children );
984}
985
986void QgsModelDesignerDialog::runFromChild( const QString &id )
987{
988 QSet<QString> children = mModel->dependentChildAlgorithms( id );
989 children.insert( id );
990 run( children );
991}
992
993void QgsModelDesignerDialog::run( const QSet<QString> &childAlgorithmSubset )
994{
995 QStringList errors;
996 const bool isValid = model()->validate( errors );
997 if ( !isValid )
998 {
999 QMessageBox messageBox;
1000 messageBox.setWindowTitle( tr( "Model is Invalid" ) );
1001 messageBox.setIcon( QMessageBox::Icon::Warning );
1002 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?" ) );
1003 messageBox.setStandardButtons( QMessageBox::StandardButton::Yes | QMessageBox::StandardButton::Cancel );
1004 messageBox.setDefaultButton( QMessageBox::StandardButton::Cancel );
1005
1006 QString errorString;
1007 for ( const QString &error : std::as_const( errors ) )
1008 {
1009 QString cleanedError = error;
1010 const thread_local QRegularExpression re( QStringLiteral( "<[^>]*>" ) );
1011 cleanedError.replace( re, QString() );
1012 errorString += QStringLiteral( "• %1\n" ).arg( cleanedError );
1013 }
1014
1015 messageBox.setDetailedText( errorString );
1016 if ( messageBox.exec() == QMessageBox::StandardButton::Cancel )
1017 return;
1018 }
1019
1020 if ( !childAlgorithmSubset.isEmpty() )
1021 {
1022 for ( const QString &child : childAlgorithmSubset )
1023 {
1024 // has user previously run all requirements for this step?
1025 const QSet<QString> requirements = mModel->dependsOnChildAlgorithms( child );
1026 for ( const QString &requirement : requirements )
1027 {
1028 if ( !mLastResult.executedChildIds().contains( requirement ) )
1029 {
1030 QMessageBox messageBox;
1031 messageBox.setWindowTitle( tr( "Run Model" ) );
1032 messageBox.setIcon( QMessageBox::Icon::Warning );
1033 messageBox.setText( tr( "Prerequisite parts of this model have not yet been run (try running the full model first)." ) );
1034 messageBox.setStandardButtons( QMessageBox::StandardButton::Ok );
1035 messageBox.exec();
1036 return;
1037 }
1038 }
1039 }
1040 }
1041
1042 std::unique_ptr<QgsProcessingAlgorithmDialogBase> dialog( createExecutionDialog() );
1043 if ( !dialog )
1044 return;
1045
1046 dialog->setLogLevel( Qgis::ProcessingLogLevel::ModelDebug );
1047 dialog->setParameters( mModel->designerParameterValues() );
1048
1049 connect( dialog.get(), &QgsProcessingAlgorithmDialogBase::algorithmAboutToRun, this, [this, &childAlgorithmSubset]( QgsProcessingContext *context ) {
1050 if ( !childAlgorithmSubset.empty() )
1051 {
1052 // start from previous state
1053 auto modelConfig = std::make_unique<QgsProcessingModelInitialRunConfig>();
1054 modelConfig->setChildAlgorithmSubset( childAlgorithmSubset );
1055 modelConfig->setPreviouslyExecutedChildAlgorithms( mLastResult.executedChildIds() );
1056 modelConfig->setInitialChildInputs( mLastResult.rawChildInputs() );
1057 modelConfig->setInitialChildOutputs( mLastResult.rawChildOutputs() );
1058
1059 // add copies of layers from previous runs to context's layer store, so that they can be used
1060 // when running the subset
1061 const QMap<QString, QgsMapLayer *> previousOutputLayers = mLayerStore.temporaryLayerStore()->mapLayers();
1062 auto previousResultStore = std::make_unique<QgsMapLayerStore>();
1063 for ( auto it = previousOutputLayers.constBegin(); it != previousOutputLayers.constEnd(); ++it )
1064 {
1065 std::unique_ptr<QgsMapLayer> clone( it.value()->clone() );
1066 clone->setId( it.value()->id() );
1067 previousResultStore->addMapLayer( clone.release() );
1068 }
1069 previousResultStore->moveToThread( nullptr );
1070 modelConfig->setPreviousLayerStore( std::move( previousResultStore ) );
1071 context->setModelInitialRunConfig( std::move( modelConfig ) );
1072 }
1073 } );
1074
1075 connect( dialog.get(), &QgsProcessingAlgorithmDialogBase::algorithmFinished, this, [this, &dialog]( bool, const QVariantMap & ) {
1076 QgsProcessingContext *context = dialog->processingContext();
1077
1078 setLastRunResult( context->modelResult() );
1079
1080 mModel->setDesignerParameterValues( dialog->createProcessingParameters( QgsProcessingParametersGenerator::Flag::SkipDefaultValueParameters ) );
1081
1082 // take child output layers
1083 mLayerStore.temporaryLayerStore()->removeAllMapLayers();
1084 mLayerStore.takeResultsFrom( *context );
1085 } );
1086
1087 dialog->exec();
1088}
1089
1090void QgsModelDesignerDialog::showChildAlgorithmOutputs( const QString &childId )
1091{
1092 const QString childDescription = mModel->childAlgorithm( childId ).description();
1093
1094 const QgsProcessingModelChildAlgorithmResult result = mLastResult.childResults().value( childId );
1095 const QVariantMap childAlgorithmOutputs = result.outputs();
1096 if ( childAlgorithmOutputs.isEmpty() )
1097 {
1098 mMessageBar->pushWarning( QString(), tr( "No results are available for %1" ).arg( childDescription ) );
1099 return;
1100 }
1101
1102 const QgsProcessingAlgorithm *algorithm = mModel->childAlgorithm( childId ).algorithm();
1103 if ( !algorithm )
1104 {
1105 mMessageBar->pushCritical( QString(), tr( "Results cannot be shown for an invalid model component" ) );
1106 return;
1107 }
1108
1109 const QList<const QgsProcessingParameterDefinition *> outputParams = algorithm->destinationParameterDefinitions();
1110 if ( outputParams.isEmpty() )
1111 {
1112 // this situation should not arise in normal use, we don't show the action in this case
1113 QgsDebugError( "Cannot show results for algorithms with no outputs" );
1114 return;
1115 }
1116
1117 bool foundResults = false;
1118 for ( const QgsProcessingParameterDefinition *outputParam : outputParams )
1119 {
1120 const QVariant output = childAlgorithmOutputs.value( outputParam->name() );
1121 if ( !output.isValid() )
1122 continue;
1123
1124 if ( output.type() == QVariant::String )
1125 {
1126 if ( QgsMapLayer *resultLayer = QgsProcessingUtils::mapLayerFromString( output.toString(), mLayerStore ) )
1127 {
1128 QgsDebugMsgLevel( QStringLiteral( "Loading previous result for %1: %2" ).arg( outputParam->name(), output.toString() ), 2 );
1129
1130 std::unique_ptr<QgsMapLayer> layer( resultLayer->clone() );
1131
1132 QString baseName;
1133 if ( outputParams.size() > 1 )
1134 baseName = tr( "%1 — %2" ).arg( childDescription, outputParam->name() );
1135 else
1136 baseName = childDescription;
1137
1138 // make name unique, so that's it's easy to see which is the most recent result.
1139 // (this helps when running the model multiple times.)
1140 QString name = baseName;
1141 int counter = 1;
1142 while ( !QgsProject::instance()->mapLayersByName( name ).empty() )
1143 {
1144 counter += 1;
1145 name = tr( "%1 (%2)" ).arg( baseName ).arg( counter );
1146 }
1147
1148 layer->setName( name );
1149
1150 QgsProject::instance()->addMapLayer( layer.release() );
1151 foundResults = true;
1152 }
1153 else
1154 {
1155 // should not happen in normal operation
1156 QgsDebugError( QStringLiteral( "Could not load previous result for %1: %2" ).arg( outputParam->name(), output.toString() ) );
1157 }
1158 }
1159 }
1160
1161 if ( !foundResults )
1162 {
1163 mMessageBar->pushWarning( QString(), tr( "No results are available for %1" ).arg( childDescription ) );
1164 return;
1165 }
1166}
1167
1168void QgsModelDesignerDialog::showChildAlgorithmLog( const QString &childId )
1169{
1170 const QString childDescription = mModel->childAlgorithm( childId ).description();
1171
1172 const QgsProcessingModelChildAlgorithmResult result = mLastResult.childResults().value( childId );
1173 if ( result.htmlLog().isEmpty() )
1174 {
1175 mMessageBar->pushWarning( QString(), tr( "No log is available for %1" ).arg( childDescription ) );
1176 return;
1177 }
1178
1179 QgsMessageViewer m( this, QgsGuiUtils::ModalDialogFlags, false );
1180 m.setWindowTitle( childDescription );
1181 m.setCheckBoxVisible( false );
1182 m.setMessageAsHtml( result.htmlLog() );
1183 m.exec();
1184}
1185
1186void QgsModelDesignerDialog::validate()
1187{
1188 QStringList issues;
1189 if ( model()->validate( issues ) )
1190 {
1191 mMessageBar->pushSuccess( QString(), tr( "Model is valid!" ) );
1192 }
1193 else
1194 {
1195 QgsMessageBarItem *messageWidget = QgsMessageBar::createMessage( QString(), tr( "Model is invalid!" ) );
1196 QPushButton *detailsButton = new QPushButton( tr( "Details" ) );
1197 connect( detailsButton, &QPushButton::clicked, detailsButton, [=] {
1198 QgsMessageViewer *dialog = new QgsMessageViewer( detailsButton );
1199 dialog->setTitle( tr( "Model is Invalid" ) );
1200
1201 QString longMessage = tr( "<p>This model is not valid:</p>" ) + QStringLiteral( "<ul>" );
1202 for ( const QString &issue : issues )
1203 {
1204 longMessage += QStringLiteral( "<li>%1</li>" ).arg( issue );
1205 }
1206 longMessage += QLatin1String( "</ul>" );
1207
1208 dialog->setMessage( longMessage, QgsMessageOutput::MessageHtml );
1209 dialog->showMessage();
1210 } );
1211 messageWidget->layout()->addWidget( detailsButton );
1212 mMessageBar->clearWidgets();
1213 mMessageBar->pushWidget( messageWidget, Qgis::MessageLevel::Warning, 0 );
1214 }
1215}
1216
1217void QgsModelDesignerDialog::reorderInputs()
1218{
1219 QgsModelInputReorderDialog dlg( this );
1220 dlg.setModel( mModel.get() );
1221 if ( dlg.exec() )
1222 {
1223 const QStringList inputOrder = dlg.inputOrder();
1224 beginUndoCommand( tr( "Reorder Inputs" ) );
1225 mModel->setParameterOrder( inputOrder );
1226 endUndoCommand();
1227 }
1228}
1229
1230void QgsModelDesignerDialog::reorderOutputs()
1231{
1232 QgsModelOutputReorderDialog dlg( this );
1233 dlg.setModel( mModel.get() );
1234 if ( dlg.exec() )
1235 {
1236 const QStringList outputOrder = dlg.outputOrder();
1237 beginUndoCommand( tr( "Reorder Outputs" ) );
1238 mModel->setOutputOrder( outputOrder );
1239 mModel->setOutputGroup( dlg.outputGroup() );
1240 endUndoCommand();
1241 }
1242}
1243
1244bool QgsModelDesignerDialog::isDirty() const
1245{
1246 return mHasChanged && mUndoStack->index() != -1;
1247}
1248
1249void QgsModelDesignerDialog::fillInputsTree()
1250{
1251 const QIcon icon = QgsApplication::getThemeIcon( QStringLiteral( "mIconModelInput.svg" ) );
1252 auto parametersItem = std::make_unique<QTreeWidgetItem>();
1253 parametersItem->setText( 0, tr( "Parameters" ) );
1254 QList<QgsProcessingParameterType *> available = QgsApplication::processingRegistry()->parameterTypes();
1255 std::sort( available.begin(), available.end(), []( const QgsProcessingParameterType *a, const QgsProcessingParameterType *b ) -> bool {
1256 return QString::localeAwareCompare( a->name(), b->name() ) < 0;
1257 } );
1258
1259 for ( QgsProcessingParameterType *param : std::as_const( available ) )
1260 {
1262 {
1263 auto paramItem = std::make_unique<QTreeWidgetItem>();
1264 paramItem->setText( 0, param->name() );
1265 paramItem->setData( 0, Qt::UserRole, param->id() );
1266 paramItem->setIcon( 0, icon );
1267 paramItem->setFlags( Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled );
1268 paramItem->setToolTip( 0, param->description() );
1269 parametersItem->addChild( paramItem.release() );
1270 }
1271 }
1272 mInputsTreeWidget->addTopLevelItem( parametersItem.release() );
1273 mInputsTreeWidget->topLevelItem( 0 )->setExpanded( true );
1274}
1275
1276
1277//
1278// QgsModelChildDependenciesWidget
1279//
1280
1281QgsModelChildDependenciesWidget::QgsModelChildDependenciesWidget( QWidget *parent, QgsProcessingModelAlgorithm *model, const QString &childId )
1282 : QWidget( parent )
1283 , mModel( model )
1284 , mChildId( childId )
1285{
1286 QHBoxLayout *hl = new QHBoxLayout();
1287 hl->setContentsMargins( 0, 0, 0, 0 );
1288
1289 mLineEdit = new QLineEdit();
1290 mLineEdit->setEnabled( false );
1291 hl->addWidget( mLineEdit, 1 );
1292
1293 mToolButton = new QToolButton();
1294 mToolButton->setText( QString( QChar( 0x2026 ) ) );
1295 hl->addWidget( mToolButton );
1296
1297 setLayout( hl );
1298
1299 mLineEdit->setText( tr( "%1 dependencies selected" ).arg( 0 ) );
1300
1301 connect( mToolButton, &QToolButton::clicked, this, &QgsModelChildDependenciesWidget::showDialog );
1302}
1303
1304void QgsModelChildDependenciesWidget::setValue( const QList<QgsProcessingModelChildDependency> &value )
1305{
1306 mValue = value;
1307
1308 updateSummaryText();
1309}
1310
1311void QgsModelChildDependenciesWidget::showDialog()
1312{
1313 const QList<QgsProcessingModelChildDependency> available = mModel->availableDependenciesForChildAlgorithm( mChildId );
1314
1315 QVariantList availableOptions;
1316 for ( const QgsProcessingModelChildDependency &dep : available )
1317 availableOptions << QVariant::fromValue( dep );
1318 QVariantList selectedOptions;
1319 for ( const QgsProcessingModelChildDependency &dep : mValue )
1320 selectedOptions << QVariant::fromValue( dep );
1321
1323 if ( panel )
1324 {
1325 QgsProcessingMultipleSelectionPanelWidget *widget = new QgsProcessingMultipleSelectionPanelWidget( availableOptions, selectedOptions );
1326 widget->setPanelTitle( tr( "Algorithm Dependencies" ) );
1327
1328 widget->setValueFormatter( [=]( const QVariant &v ) -> QString {
1329 const QgsProcessingModelChildDependency dep = v.value<QgsProcessingModelChildDependency>();
1330
1331 const QString description = mModel->childAlgorithm( dep.childId ).description();
1332 if ( dep.conditionalBranch.isEmpty() )
1333 return description;
1334 else
1335 return tr( "Condition “%1” from algorithm “%2”" ).arg( dep.conditionalBranch, description );
1336 } );
1337
1338 connect( widget, &QgsProcessingMultipleSelectionPanelWidget::selectionChanged, this, [=]() {
1339 QList<QgsProcessingModelChildDependency> res;
1340 for ( const QVariant &v : widget->selectedOptions() )
1341 {
1342 res << v.value<QgsProcessingModelChildDependency>();
1343 }
1344 setValue( res );
1345 } );
1346 connect( widget, &QgsProcessingMultipleSelectionPanelWidget::acceptClicked, widget, &QgsPanelWidget::acceptPanel );
1347 panel->openPanel( widget );
1348 }
1349}
1350
1351void QgsModelChildDependenciesWidget::updateSummaryText()
1352{
1353 mLineEdit->setText( tr( "%n dependencies selected", nullptr, mValue.count() ) );
1354}
1355
@ 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