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