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