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