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