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