QGIS API Documentation 4.3.0-Master (52aa6503bed)
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 "qgscodeeditorpython.h"
23#include "qgsfileutils.h"
24#include "qgsgui.h"
25#include "qgsmessagebar.h"
26#include "qgsmessagebaritem.h"
27#include "qgsmessagelog.h"
28#include "qgsmessageviewer.h"
33#include "qgsmodelundocommand.h"
34#include "qgsmodelviewtoolpan.h"
36#include "qgspanelwidget.h"
50#include "qgsproject.h"
51#include "qgsscreenhelper.h"
52#include "qgssettings.h"
53
54#include <QActionGroup>
55#include <QCloseEvent>
56#include <QFileDialog>
57#include <QKeySequence>
58#include <QMessageBox>
59#include <QPdfWriter>
60#include <QPushButton>
61#include <QShortcut>
62#include <QString>
63#include <QSvgGenerator>
64#include <QTextStream>
65#include <QTimer>
66#include <QToolButton>
67#include <QUndoView>
68#include <QUrl>
69
70#include "moc_qgsmodeldesignerdialog.cpp"
71
72using namespace Qt::StringLiterals;
73
75
76
77QgsModelerToolboxModel::QgsModelerToolboxModel( QObject *parent )
79{}
80
81Qt::ItemFlags QgsModelerToolboxModel::flags( const QModelIndex &index ) const
82{
83 Qt::ItemFlags f = QgsProcessingToolboxProxyModel::flags( index );
84 const QModelIndex sourceIndex = mapToSource( index );
85 if ( toolboxModel()->isAlgorithm( sourceIndex ) || toolboxModel()->isParameter( sourceIndex ) )
86 {
87 f = f | Qt::ItemIsDragEnabled;
88 }
89 return f;
90}
91
92Qt::DropActions QgsModelerToolboxModel::supportedDragActions() const
93{
94 return Qt::CopyAction;
95}
96
97QgsModelDesignerDialog::QgsModelDesignerDialog( QWidget *parent, Qt::WindowFlags flags )
98 : QMainWindow( parent, flags )
99 , mContext( QgsGui::processingGuiRegistry()->contextFactory() ? QgsGui::processingGuiRegistry()->contextFactory()->createContext() : new QgsProcessingContext() )
100 , mToolsActionGroup( new QActionGroup( this ) )
101{
102 setupUi( this );
103
104 mToolbar->setIconSize( QgsGui::iconSize() );
105 setStyleSheet( QgsGui::applicationStyleSheet() );
106 connect( QgsGui::instance(), &QgsGui::applicationStyleSheetChanged, this, &QgsModelDesignerDialog::setStyleSheet );
107
108 mLayerStore.setProject( QgsProject::instance() );
109
110 mScreenHelper = new QgsScreenHelper( this );
111
112 setAttribute( Qt::WA_DeleteOnClose );
113 setDockOptions( dockOptions() | QMainWindow::GroupedDragging );
114 setWindowFlags( Qt::WindowMinimizeButtonHint | Qt::WindowMaximizeButtonHint | Qt::WindowCloseButtonHint );
115
117
118 mModel = std::make_unique<QgsProcessingModelAlgorithm>();
119 mModel->setProvider( QgsApplication::processingRegistry()->providerById( QgsProcessing::MODEL_PROVIDER_ID ) );
120
121 mUndoStack = new QUndoStack( this );
122 connect( mUndoStack, &QUndoStack::indexChanged, this, [this] {
123 if ( mIgnoreUndoStackChanges )
124 return;
125
126 mBlockUndoCommands++;
127 updateVariablesGui();
128 mGroupEdit->setText( mModel->group() );
129 mNameEdit->setText( mModel->displayName() );
130 mBlockUndoCommands--;
131 repaintModel();
132 } );
133
134 mConfigWidgetDock = new QgsDockWidget( this );
135 mConfigWidgetDock->setWindowTitle( tr( "Configuration" ) );
136 mConfigWidgetDock->setObjectName( u"ModelConfigDock"_s );
137
138 mConfigWidget = new QgsModelDesignerConfigDockWidget();
139 mConfigWidgetDock->setWidget( mConfigWidget );
140 mConfigWidgetDock->setFeatures( QDockWidget::NoDockWidgetFeatures );
141 addDockWidget( Qt::RightDockWidgetArea, mConfigWidgetDock );
142
143 mPropertiesDock->setFeatures( QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetClosable );
144 mInputsDock->setFeatures( QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetClosable );
145 mAlgorithmsDock->setFeatures( QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetClosable );
146 mVariablesDock->setFeatures( QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetClosable );
147
148 mToolboxTree->header()->setVisible( false );
149 mToolboxSearchEdit->setShowSearchIcon( true );
150 mToolboxSearchEdit->setPlaceholderText( tr( "Search…" ) );
151 connect( mToolboxSearchEdit, &QgsFilterLineEdit::textChanged, mToolboxTree, &QgsProcessingToolboxTreeView::setFilterString );
152
153 mInputsTreeWidget->header()->setVisible( false );
154 mInputsTreeWidget->setAlternatingRowColors( true );
155 mInputsTreeWidget->setDragDropMode( QTreeWidget::DragOnly );
156 mInputsTreeWidget->setDropIndicatorShown( true );
157
158 mNameEdit->setPlaceholderText( tr( "Enter model name here" ) );
159 mGroupEdit->setPlaceholderText( tr( "Enter group name here" ) );
160
161 mMessageBar = new QgsMessageBar();
162 mMessageBar->setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Fixed );
163 mainLayout->insertWidget( 0, mMessageBar );
164
165 mView->setAcceptDrops( true );
166 QgsSettings settings;
167
168 connect( mActionClose, &QAction::triggered, this, &QWidget::close );
169 connect( mActionNew, &QAction::triggered, this, &QgsModelDesignerDialog::newModel );
170 connect( mActionZoomIn, &QAction::triggered, this, &QgsModelDesignerDialog::zoomIn );
171 connect( mActionZoomOut, &QAction::triggered, this, &QgsModelDesignerDialog::zoomOut );
172 connect( mActionZoomActual, &QAction::triggered, this, &QgsModelDesignerDialog::zoomActual );
173 connect( mActionZoomToItems, &QAction::triggered, this, &QgsModelDesignerDialog::zoomFull );
174 connect( mActionExportImage, &QAction::triggered, this, &QgsModelDesignerDialog::exportToImage );
175 connect( mActionExportPdf, &QAction::triggered, this, &QgsModelDesignerDialog::exportToPdf );
176 connect( mActionExportSvg, &QAction::triggered, this, &QgsModelDesignerDialog::exportToSvg );
177 connect( mActionExportPython, &QAction::triggered, this, &QgsModelDesignerDialog::exportAsPython );
178 connect( mActionOpen, &QAction::triggered, this, &QgsModelDesignerDialog::openModel );
179 connect( mActionSave, &QAction::triggered, this, [this] { saveModel( false ); } );
180 connect( mActionSaveAs, &QAction::triggered, this, [this] { saveModel( true ); } );
181 connect( mActionSaveInProject, &QAction::triggered, this, &QgsModelDesignerDialog::saveInProject );
182 connect( mActionDeleteComponents, &QAction::triggered, this, &QgsModelDesignerDialog::deleteSelected );
183 connect( mActionSnapSelected, &QAction::triggered, mView, &QgsModelGraphicsView::snapSelected );
184 connect( mActionValidate, &QAction::triggered, this, &QgsModelDesignerDialog::validate );
185 connect( mActionReorderInputs, &QAction::triggered, this, &QgsModelDesignerDialog::reorderInputs );
186 connect( mActionReorderOutputs, &QAction::triggered, this, &QgsModelDesignerDialog::reorderOutputs );
187 connect( mActionEditHelp, &QAction::triggered, this, &QgsModelDesignerDialog::editHelp );
188 connect( mReorderInputsButton, &QPushButton::clicked, this, &QgsModelDesignerDialog::reorderInputs );
189 connect( mActionRun, &QAction::triggered, this, [this] { run(); } );
190 connect( mActionRunSelectedSteps, &QAction::triggered, this, &QgsModelDesignerDialog::runSelectedSteps );
191
192 mActionSnappingEnabled->setChecked( settings.value( u"/Processing/Modeler/enableSnapToGrid"_s, false ).toBool() );
193 connect( mActionSnappingEnabled, &QAction::toggled, this, [this]( bool enabled ) {
194 mView->snapper()->setSnapToGrid( enabled );
195 QgsSettings().setValue( u"/Processing/Modeler/enableSnapToGrid"_s, enabled );
196 } );
197 mView->snapper()->setSnapToGrid( mActionSnappingEnabled->isChecked() );
198
199 connect( mView, &QgsModelGraphicsView::itemFocused, this, &QgsModelDesignerDialog::onItemFocused );
200
201 connect( mActionSelectAll, &QAction::triggered, this, [this] { mScene->selectAll(); } );
202
203 QStringList docksTitle = settings.value( u"ModelDesigner/hiddenDocksTitle"_s, QStringList(), QgsSettings::App ).toStringList();
204 QStringList docksActive = settings.value( u"ModelDesigner/hiddenDocksActive"_s, QStringList(), QgsSettings::App ).toStringList();
205 if ( !docksTitle.isEmpty() )
206 {
207 for ( const auto &title : docksTitle )
208 {
209 mPanelStatus.insert( title, PanelStatus( true, docksActive.contains( title ) ) );
210 }
211 }
212 mActionHidePanels->setChecked( !docksTitle.isEmpty() );
213 connect( mActionHidePanels, &QAction::toggled, this, &QgsModelDesignerDialog::setPanelVisibility );
214
215 mUndoAction = mUndoStack->createUndoAction( this );
216 mUndoAction->setIcon( QgsApplication::getThemeIcon( u"/mActionUndo.svg"_s ) );
217 mUndoAction->setShortcuts( QKeySequence::Undo );
218 mRedoAction = mUndoStack->createRedoAction( this );
219 mRedoAction->setIcon( QgsApplication::getThemeIcon( u"/mActionRedo.svg"_s ) );
220 mRedoAction->setShortcuts( QKeySequence::Redo );
221
222 mMenuEdit->insertAction( mActionDeleteComponents, mRedoAction );
223 mMenuEdit->insertAction( mActionDeleteComponents, mUndoAction );
224 mMenuEdit->insertSeparator( mActionDeleteComponents );
225 mToolbar->insertAction( mActionZoomIn, mUndoAction );
226 mToolbar->insertAction( mActionZoomIn, mRedoAction );
227 mToolbar->insertSeparator( mActionZoomIn );
228
229 mGroupMenu = new QMenu( tr( "Zoom To" ), this );
230 mMenuView->insertMenu( mActionZoomIn, mGroupMenu );
231 connect( mGroupMenu, &QMenu::aboutToShow, this, &QgsModelDesignerDialog::populateZoomToMenu );
232
233 //cut/copy/paste actions. Note these are not included in the ui file
234 //as ui files have no support for QKeySequence shortcuts
235 mActionCut = new QAction( tr( "Cu&t" ), this );
236 mActionCut->setShortcuts( QKeySequence::Cut );
237 mActionCut->setStatusTip( tr( "Cut" ) );
238 mActionCut->setIcon( QgsApplication::getThemeIcon( u"/mActionEditCut.svg"_s ) );
239 connect( mActionCut, &QAction::triggered, this, [this] { mView->copySelectedItems( QgsModelGraphicsView::ClipboardCut ); } );
240
241 mActionCopy = new QAction( tr( "&Copy" ), this );
242 mActionCopy->setShortcuts( QKeySequence::Copy );
243 mActionCopy->setStatusTip( tr( "Copy" ) );
244 mActionCopy->setIcon( QgsApplication::getThemeIcon( u"/mActionEditCopy.svg"_s ) );
245 connect( mActionCopy, &QAction::triggered, this, [this] { mView->copySelectedItems( QgsModelGraphicsView::ClipboardCopy ); } );
246
247 mActionPaste = new QAction( tr( "&Paste" ), this );
248 mActionPaste->setShortcuts( QKeySequence::Paste );
249 mActionPaste->setStatusTip( tr( "Paste" ) );
250 mActionPaste->setIcon( QgsApplication::getThemeIcon( u"/mActionEditPaste.svg"_s ) );
251 connect( mActionPaste, &QAction::triggered, this, [this] { mView->pasteItems( QgsModelGraphicsView::PasteModeCursor ); } );
252 mMenuEdit->insertAction( mActionDeleteComponents, mActionCut );
253 mMenuEdit->insertAction( mActionDeleteComponents, mActionCopy );
254 mMenuEdit->insertAction( mActionDeleteComponents, mActionPaste );
255 mMenuEdit->insertSeparator( mActionDeleteComponents );
256
257 mAlgorithmsModel = new QgsModelerToolboxModel( this );
258 mToolboxTree->setToolboxProxyModel( mAlgorithmsModel );
259
261 if ( settings.value( u"Processing/Configuration/SHOW_ALGORITHMS_KNOWN_ISSUES"_s, false ).toBool() )
262 {
264 }
265 mToolboxTree->setFilters( filters );
266 mToolboxTree->setDragDropMode( QTreeWidget::DragOnly );
267 mToolboxTree->setDropIndicatorShown( true );
268
269 connect( mView, &QgsModelGraphicsView::algorithmDropped, this, [this]( const QString &algorithmId, const QPointF &pos ) { addAlgorithm( algorithmId, pos ); } );
270 connect( mView, &QgsModelGraphicsView::inputDropped, this, &QgsModelDesignerDialog::addInput );
271
272 connect( mToolboxTree, &QgsProcessingToolboxTreeView::doubleClicked, this, [this]( const QModelIndex & ) {
273 if ( mToolboxTree->selectedAlgorithm() )
274 addAlgorithm( mToolboxTree->selectedAlgorithm()->id(), QPointF() );
275 if ( mToolboxTree->selectedParameterType() )
276 addInput( mToolboxTree->selectedParameterType()->id(), QPointF() );
277 } );
278
279 connect( mInputsTreeWidget, &QgsModelDesignerInputsTreeWidget::doubleClicked, this, [this]( const QModelIndex & ) {
280 const QString parameterType = mInputsTreeWidget->currentItem()->data( 0, Qt::UserRole ).toString();
281 addInput( parameterType, QPointF() );
282 } );
283
284 // Ctrl+= should also trigger a zoom in action
285 QShortcut *ctrlEquals = new QShortcut( QKeySequence( u"Ctrl+="_s ), this );
286 connect( ctrlEquals, &QShortcut::activated, this, &QgsModelDesignerDialog::zoomIn );
287
288 mUndoDock = new QgsDockWidget( tr( "Undo History" ), this );
289 mUndoDock->setObjectName( u"UndoDock"_s );
290 mUndoView = new QUndoView( mUndoStack, this );
291 mUndoDock->setWidget( mUndoView );
292 mUndoDock->setFeatures( QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetClosable );
293 addDockWidget( Qt::DockWidgetArea::LeftDockWidgetArea, mUndoDock );
294
295 tabifyDockWidget( mUndoDock, mPropertiesDock );
296 tabifyDockWidget( mVariablesDock, mPropertiesDock );
297 mPropertiesDock->raise();
298 tabifyDockWidget( mInputsDock, mAlgorithmsDock );
299 mInputsDock->raise();
300
301 connect( mVariablesEditor, &QgsVariableEditorWidget::scopeChanged, this, [this] {
302 if ( mModel )
303 {
304 beginUndoCommand( tr( "Change Model Variables" ) );
305 mModel->setVariables( mVariablesEditor->variablesInActiveScope() );
306 endUndoCommand();
307 }
308 } );
309 connect( mNameEdit, &QLineEdit::textChanged, this, [this]( const QString &name ) {
310 if ( mModel )
311 {
312 beginUndoCommand( tr( "Change Model Name" ), QString(), QgsModelUndoCommand::CommandOperation::NameChanged );
313 mModel->setName( name );
314 endUndoCommand();
315 updateWindowTitle();
316 }
317 } );
318 connect( mGroupEdit, &QLineEdit::textChanged, this, [this]( const QString &group ) {
319 if ( mModel )
320 {
321 beginUndoCommand( tr( "Change Model Group" ), QString(), QgsModelUndoCommand::CommandOperation::GroupChanged );
322 mModel->setGroup( group );
323 endUndoCommand();
324 updateWindowTitle();
325 }
326 } );
327
328 fillInputsTree();
329
330 QToolButton *toolbuttonExportToScript = new QToolButton();
331 toolbuttonExportToScript->setPopupMode( QToolButton::InstantPopup );
332 toolbuttonExportToScript->addAction( mActionExportAsScriptAlgorithm );
333 toolbuttonExportToScript->setDefaultAction( mActionExportAsScriptAlgorithm );
334 mToolbar->insertWidget( mActionExportImage, toolbuttonExportToScript );
335 connect( mActionExportAsScriptAlgorithm, &QAction::triggered, this, &QgsModelDesignerDialog::exportAsScriptAlgorithm );
336
337 mActionShowComments->setChecked( settings.value( u"/Processing/Modeler/ShowComments"_s, true ).toBool() );
338 connect( mActionShowComments, &QAction::toggled, this, &QgsModelDesignerDialog::toggleComments );
339
340 mActionShowFeatureCount->setChecked( settings.value( u"/Processing/Modeler/ShowFeatureCount"_s, true ).toBool() );
341 connect( mActionShowFeatureCount, &QAction::toggled, this, &QgsModelDesignerDialog::toggleFeatureCount );
342
343 mPanTool = new QgsModelViewToolPan( mView );
344 mPanTool->setAction( mActionPan );
345
346 mToolsActionGroup->addAction( mActionPan );
347 connect( mActionPan, &QAction::triggered, mPanTool, [this] { mView->setTool( mPanTool ); } );
348
349 // We use a QObjectUniquePtr here because we want to delete QgsModelViewToolSelect
350 // mouse handles before everything else and don't want to wait for QObject destructor to destroy it
351 mSelectTool = make_qobject_unique<QgsModelViewToolSelect>( mView );
352 mSelectTool->setAction( mActionSelectMoveItem );
353
354 mToolsActionGroup->addAction( mActionSelectMoveItem );
355 connect( mActionSelectMoveItem, &QAction::triggered, mSelectTool, [this] { mView->setTool( mSelectTool ); } );
356
357 mView->setTool( mSelectTool );
358 mView->setFocus();
359
360 connect( mView, &QgsModelGraphicsView::macroCommandStarted, this, [this]( const QString &text ) {
361 mIgnoreUndoStackChanges++;
362 mUndoStack->beginMacro( text );
363 mIgnoreUndoStackChanges--;
364 } );
365 connect( mView, &QgsModelGraphicsView::macroCommandEnded, this, [this] {
366 mIgnoreUndoStackChanges++;
367 mUndoStack->endMacro();
368 mIgnoreUndoStackChanges--;
369 } );
370 connect( mView, &QgsModelGraphicsView::commandBegun, this, [this]( const QString &text ) { beginUndoCommand( text ); } );
371 connect( mView, &QgsModelGraphicsView::commandEnded, this, [this] { endUndoCommand(); } );
372 connect( mView, &QgsModelGraphicsView::commandAborted, this, [this] { abortUndoCommand(); } );
373 connect( mView, &QgsModelGraphicsView::deleteSelectedItems, this, [this] { deleteSelected(); } );
374
375 connect( mActionAddGroupBox, &QAction::triggered, this, [this] {
376 const QPointF viewCenter = mView->mapToScene( mView->viewport()->rect().center() );
377 QgsProcessingModelGroupBox group;
378 group.setPosition( viewCenter );
379 group.setDescription( tr( "New Group" ) );
380
381 beginUndoCommand( tr( "Add Group Box" ) );
382 model()->addGroupBox( group );
383 repaintModel();
384 endUndoCommand();
385 } );
386
387 updateWindowTitle();
388
389 // restore the toolbar and dock widgets positions using Qt settings API
390 restoreState( settings.value( u"ModelDesigner/state"_s, QByteArray(), QgsSettings::App ).toByteArray() );
391}
392
393QgsModelDesignerDialog::~QgsModelDesignerDialog()
394{
395 if ( mAlgorithmWidget )
396 {
397 delete mAlgorithmWidget;
398 }
399 for ( const QPointer<QgsProcessingAlgorithmWidget> &widget : std::as_const( mAlgorithmWidgetsToCleanUp ) )
400 {
401 // this is a work around for the MESSY ownership issues associated with the python subclass
402 // of QgsProcessingAlgorithmWidget. We have to FORCE all widgets to be deleted prior
403 // to destruction of this window, and we can't be sure that python will have actually
404 // deleted the widget when we asked...
405 if ( widget )
406 {
407 delete widget;
408 }
409 }
410
411 QgsSettings settings;
412 if ( !mPanelStatus.isEmpty() )
413 {
414 QStringList docksTitle;
415 QStringList docksActive;
416
417 for ( const auto &panel : mPanelStatus.toStdMap() )
418 {
419 if ( panel.second.isVisible )
420 docksTitle << panel.first;
421 if ( panel.second.isActive )
422 docksActive << panel.first;
423 }
424 settings.setValue( u"ModelDesigner/hiddenDocksTitle"_s, docksTitle, QgsSettings::App );
425 settings.setValue( u"ModelDesigner/hiddenDocksActive"_s, docksActive, QgsSettings::App );
426 }
427 else
428 {
429 settings.remove( u"ModelDesigner/hiddenDocksTitle"_s, QgsSettings::App );
430 settings.remove( u"ModelDesigner/hiddenDocksActive"_s, QgsSettings::App );
431 }
432
433 // store the toolbar/dock widget settings using Qt settings API
434 settings.setValue( u"ModelDesigner/state"_s, saveState(), QgsSettings::App );
435
436 mIgnoreUndoStackChanges++;
437}
438
439void QgsModelDesignerDialog::closeEvent( QCloseEvent *event )
440{
441 if ( checkForUnsavedChanges() )
442 event->accept();
443 else
444 event->ignore();
445}
446
447void QgsModelDesignerDialog::beginUndoCommand( const QString &text, const QString &id, QgsModelUndoCommand::CommandOperation operation )
448{
449 if ( mBlockUndoCommands || !mUndoStack )
450 return;
451
452 if ( mActiveCommand )
453 endUndoCommand();
454
455 if ( !id.isEmpty() )
456 {
457 mActiveCommand = std::make_unique<QgsModelUndoCommand>( mModel.get(), text, id );
458 }
459 else
460 {
461 mActiveCommand = std::make_unique<QgsModelUndoCommand>( mModel.get(), text, operation );
462 }
463}
464
465void QgsModelDesignerDialog::endUndoCommand()
466{
467 if ( mBlockUndoCommands || !mActiveCommand || !mUndoStack )
468 return;
469
470 mActiveCommand->saveAfterState();
471 mIgnoreUndoStackChanges++;
472 mUndoStack->push( mActiveCommand.release() );
473 mIgnoreUndoStackChanges--;
474 setDirty( true );
475}
476
477void QgsModelDesignerDialog::abortUndoCommand()
478{
479 if ( mActiveCommand )
480 mActiveCommand->setObsolete( true );
481}
482
483QgsProcessingModelAlgorithm *QgsModelDesignerDialog::model()
484{
485 return mModel.get();
486}
487
488void QgsModelDesignerDialog::setModel( QgsProcessingModelAlgorithm *model )
489{
490 mModel.reset( model );
491
492 mGroupEdit->setText( mModel->group() );
493 mNameEdit->setText( mModel->displayName() );
494 repaintModel( true );
495 updateVariablesGui();
496
497 const QString modelQgisVersion = mModel->modelQgisVersion();
498 handleVersionMismatch( modelQgisVersion );
499
500 setDirty( false );
501
502 mIgnoreUndoStackChanges++;
503 mUndoStack->clear();
504 mIgnoreUndoStackChanges--;
505
506 updateWindowTitle();
507
508 // Delay zoom to the full model to ensure the scene has been properly set
509 // and that the itemsBoundingRect returns the correct value.
510 QTimer::singleShot( 100, this, [this] { zoomFull(); } );
511}
512
513void QgsModelDesignerDialog::loadModel( const QString &path )
514{
515 auto alg = std::make_unique<QgsProcessingModelAlgorithm>();
516 if ( alg->fromFile( path ) )
517 {
518 alg->setProvider( QgsApplication::processingRegistry()->providerById( QgsProcessing::MODEL_PROVIDER_ID ) );
519 alg->setSourceFilePath( path );
520 setModel( alg.release() );
521 }
522 else
523 {
524 QgsMessageLog::logMessage( tr( "Could not load model %1" ).arg( path ), tr( "Processing" ), Qgis::MessageLevel::Critical );
525 QMessageBox::critical(
526 this,
527 tr( "Open Model" ),
528 tr(
529 "The selected model could not be loaded.\n"
530 "See the log for more information."
531 )
532 );
533 }
534}
535
536void QgsModelDesignerDialog::handleVersionMismatch( const QString &modelVersion ) const
537{
538 const QgsProjectVersion thisVersion( Qgis::version() );
539
540 const QgsProjectVersion fileVersion = ( modelVersion.isEmpty() ) ? QgsProjectVersion( 0, 0, 0, QString() ) : QgsProjectVersion( modelVersion );
541
542 // Special case before 4.4 we didn't store the qgis version in the model so assume it's before 4.4
543 if ( fileVersion == QgsProjectVersion( 0, 0, 0 ) )
544 {
545 const QString message = tr(
546 "This model file was created in an older QGIS version."
547 " When saving this model file, QGIS will update it to version %1, "
548 "possibly rendering it unusable with older versions of QGIS."
549 )
550 .arg( thisVersion.text() );
551 mMessageBar->pushInfo( QString(), message );
552 }
553 else if ( thisVersion > fileVersion )
554 {
555 const QString message = tr(
556 "This model file was created in QGIS version %1."
557 " When saving this model file, QGIS will update it to version %2, "
558 "possibly rendering it unusable with older versions of QGIS."
559 )
560 .arg( fileVersion.text(), thisVersion.text() );
561 mMessageBar->pushInfo( QString(), message );
562 }
563 else if ( thisVersion < fileVersion )
564 {
565 const QString message = tr(
566 "This model was created by a newer version of QGIS (%1)."
567 "The integrity of this model file may not be guaranteed."
568 )
569 .arg( fileVersion.text() );
570
571 mMessageBar->pushWarning( QString(), message );
572 }
573 else
574 {
575 //All good model and qgis version match!
576 }
577}
578
579void QgsModelDesignerDialog::setModelScene( QgsModelGraphicsScene *scene )
580{
581 QgsModelGraphicsScene *oldScene = mScene;
582
583 mScene = scene;
584 mScene->setParent( this );
585 mScene->setLastRunResult( mLastResult, mLayerStore );
586 mScene->setModel( mModel.get() );
587 mScene->setMessageBar( mMessageBar );
588 mScene->registerWidgetContextGenerator( this );
589
590 QgsSettings settings;
591 const bool showFeatureCount = settings.value( u"/Processing/Modeler/ShowFeatureCount"_s, true ).toBool();
592 if ( !showFeatureCount )
593 mScene->setFlag( QgsModelGraphicsScene::FlagHideFeatureCount );
594
595 mView->setModelScene( mScene );
596
597 mSelectTool->resetCache();
598 mSelectTool->setScene( mScene );
599
600 connect( mScene, &QgsModelGraphicsScene::rebuildRequired, this, [this] {
601 if ( mBlockRepaints )
602 return;
603
604 repaintModel();
605 mScene->flagChildrenAsOutdated( mOutdatedChildResults );
606 } );
607 connect( mScene, &QgsModelGraphicsScene::componentAboutToChange, this, [this]( const QString &description, const QString &id ) { beginUndoCommand( description, id ); } );
608 connect( mScene, &QgsModelGraphicsScene::componentChanged, this, [this] { endUndoCommand(); } );
609 connect( mScene, &QgsModelGraphicsScene::runFromChild, this, &QgsModelDesignerDialog::runFromChild );
610 connect( mScene, &QgsModelGraphicsScene::runSelected, this, &QgsModelDesignerDialog::runSelectedSteps );
611 connect( mScene, &QgsModelGraphicsScene::showChildAlgorithmOutputs, this, &QgsModelDesignerDialog::showChildAlgorithmOutputs );
612 connect( mScene, &QgsModelGraphicsScene::showChildAlgorithmLog, this, &QgsModelDesignerDialog::showChildAlgorithmLog );
613
614 if ( oldScene )
615 oldScene->deleteLater();
616}
617
618QgsModelGraphicsScene *QgsModelDesignerDialog::modelScene()
619{
620 return mScene;
621}
622
623QgsProcessingFeedback *QgsModelDesignerDialog::createFeedback()
624{
625 auto result = std::make_unique< QgsProcessingModelFeedback >();
626 mScene->setupFeedbackConnections( result.get() );
627 connect( result.get(), &QgsProcessingModelFeedback::childResultReported, this, [this]( const QString &childId, const QgsProcessingModelChildAlgorithmResult & ) {
628 mOutdatedChildResults.remove( childId );
629 } );
630
631 return result.release();
632}
633
634QgsProcessingParameterWidgetContext QgsModelDesignerDialog::createWidgetContext()
635{
637 context.setModel( model() );
638 context.setModelDesignerDialog( this );
640 return context;
641}
642
643void QgsModelDesignerDialog::activate()
644{
645 show();
646 raise();
647 setWindowState( windowState() & ~Qt::WindowMinimized );
648 activateWindow();
649}
650
651QgsProcessingContext *QgsModelDesignerDialog::processingContext() const
652{
653 return mContext.get();
654}
655
656void QgsModelDesignerDialog::updateVariablesGui()
657{
658 mBlockUndoCommands++;
659
660 auto variablesScope = std::make_unique<QgsExpressionContextScope>( tr( "Model Variables" ) );
661 const QVariantMap modelVars = mModel->variables();
662 for ( auto it = modelVars.constBegin(); it != modelVars.constEnd(); ++it )
663 {
664 variablesScope->setVariable( it.key(), it.value() );
665 }
666 QgsExpressionContext variablesContext;
667 variablesContext.appendScope( variablesScope.release() );
668 mVariablesEditor->setContext( &variablesContext );
669 mVariablesEditor->setEditableScopeIndex( 0 );
670
671 mBlockUndoCommands--;
672}
673
674void QgsModelDesignerDialog::setDirty( bool dirty )
675{
676 mHasChanged = dirty;
677 updateWindowTitle();
678 if ( mAlgorithmWidget )
679 {
680 if ( QgsMessageBar *messageBar = mAlgorithmWidget->messageBar() )
681 {
682 QgsMessageBarItem *messageBarItem = messageBar->createMessage( QString(), tr( "The model has changed, this panel should be reloaded." ) );
683 auto reloadButton = new QPushButton( tr( "Reload Now" ) );
684 connect( reloadButton, &QPushButton::clicked, reloadButton, [this] {
685 if ( mAlgorithmWidget && mAlgorithmWidget->isRunning() )
686 {
687 QMessageBox messageBox;
688 messageBox.setIcon( QMessageBox::Icon::Warning );
689 messageBox.setWindowTitle( tr( "Run Model" ) );
690 messageBox.setText( tr( "This model is currently running." ) );
691 messageBox.setStandardButtons( QMessageBox::StandardButton::Cancel | QMessageBox::StandardButton::RestoreDefaults );
692
693 QAbstractButton *buttonReRun = messageBox.button( QMessageBox::StandardButton::RestoreDefaults );
694 buttonReRun->setText( tr( "Terminate and Reload" ) );
695
696 int r = messageBox.exec();
697
698 switch ( r )
699 {
700 case QMessageBox::StandardButton::Cancel:
701 return;
702 case QMessageBox::StandardButton::RestoreDefaults:
703 break;
704 default:
705 break;
706 }
707 }
708 cancelRunningModel();
709 run();
710 } );
711 messageBarItem->layout()->addWidget( reloadButton );
712 messageBar->pushWidget( messageBarItem, Qgis::MessageLevel::Warning );
713 }
714 }
715}
716
717bool QgsModelDesignerDialog::validateSave( SaveAction action )
718{
719 switch ( action )
720 {
721 case QgsModelDesignerDialog::SaveAction::SaveAsFile:
722 break;
723 case QgsModelDesignerDialog::SaveAction::SaveInProject:
724 if ( mNameEdit->text().trimmed().isEmpty() )
725 {
726 mMessageBar->pushWarning( QString(), tr( "Please enter a model name before saving" ) );
727 return false;
728 }
729 break;
730 }
731
732 return true;
733}
734
735bool QgsModelDesignerDialog::checkForUnsavedChanges()
736{
737 if ( isDirty() )
738 {
739 QMessageBox::StandardButton ret = QMessageBox::
740 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 );
741 switch ( ret )
742 {
743 case QMessageBox::Save:
744 return saveModel( false );
745
746 case QMessageBox::Discard:
747 return true;
748
749 default:
750 return false;
751 }
752 }
753 else
754 {
755 return true;
756 }
757}
758
759void QgsModelDesignerDialog::setLastRunResult( const QgsProcessingModelResult &result )
760{
761 mLastResult.mergeWith( result );
762 if ( mScene )
763 mScene->setLastRunResult( mLastResult, mLayerStore );
764}
765
766void QgsModelDesignerDialog::setModelName( const QString &name )
767{
768 mNameEdit->setText( name );
769}
770
771void QgsModelDesignerDialog::saveInProject()
772{
773 if ( !validateSave( SaveAction::SaveInProject ) )
774 return;
775
776 mModel->setSourceFilePath( QString() );
777
778 auto projectProvider = qobject_cast< QgsProcessingProjectModelProvider * >( QgsApplication::processingRegistry()->providerById( QgsProcessing::PROJECT_PROVIDER_ID ) );
779 if ( !projectProvider )
780 return; // should not happen
781
782 projectProvider->addModel( *mModel );
783
784 emit modelUpdated();
785
786 mMessageBar->pushMessage( QString(), tr( "Model was saved inside current project" ), Qgis::MessageLevel::Success, 5 );
787
788 setDirty( false );
790}
791
792bool QgsModelDesignerDialog::saveModel( bool saveAs )
793{
794 if ( !validateSave( SaveAction::SaveAsFile ) )
795 return false;
796
797 const bool modelNameMatchedFileName = mModel->modelNameMatchesFilePath();
798 QString fileName;
799 if ( !mModel->sourceFilePath().isEmpty() && !saveAs )
800 {
801 fileName = mModel->sourceFilePath();
802 }
803 else
804 {
805 QString initialPath;
806 if ( !mModel->sourceFilePath().isEmpty() )
807 {
808 initialPath = mModel->sourceFilePath();
809 }
810 else if ( !mModel->name().isEmpty() )
811 {
812 initialPath = u"%1/%2.model3"_s.arg( QgsProcessingUtils::modelFolders()[0], mModel->name() );
813 }
814 else
815 {
816 initialPath = QgsProcessingUtils::modelFolders()[0];
817 }
818
819 fileName = QFileDialog::getSaveFileName( this, tr( "Save Model" ), initialPath, tr( "Processing models (*.model3 *.MODEL3)" ) );
820 if ( fileName.isEmpty() )
821 {
822 return false;
823 }
824
825 fileName = QgsFileUtils::ensureFileNameHasExtension( fileName, { "model3" } );
826 mModel->setSourceFilePath( fileName );
827
828 if ( mModel->name().isEmpty() || mModel->name() == tr( "model" ) )
829 {
830 setModelName( QFileInfo( fileName ).baseName() );
831 }
832 else if ( saveAs && modelNameMatchedFileName )
833 {
834 // if saving as, and the model name used to match the filename, then automatically update the
835 // model name to match the new file name
836 setModelName( QFileInfo( fileName ).baseName() );
837 }
838 }
839
840 if ( !mModel->toFile( fileName ) )
841 {
842 if ( saveAs )
843 {
844 QMessageBox::warning( this, tr( "Save Model" ), tr( "Unable to save edits (probably you do not have permission to write to this location)." ) );
845 }
846 else
847 {
848 QMessageBox::warning(
849 this,
850 tr( "Save Model" ),
851 tr(
852 "This model can't be saved in its original location (probably you do not "
853 "have permission to do it). Please, use the 'Save as…' option."
854 )
855 );
856 }
857 return false;
858 }
859
860 emit modelUpdated();
861 if ( saveAs )
862 {
863 mMessageBar->pushMessage( QString(), tr( "Model was saved to <a href=\"%1\">%2</a>" ).arg( QUrl::fromLocalFile( fileName ).toString(), QDir::toNativeSeparators( fileName ) ), Qgis::MessageLevel::Success, 5 );
864 }
865
866 setDirty( false );
867 return true;
868}
869
870QPointF QgsModelDesignerDialog::getPositionForParameterItem() const
871{
872 constexpr double MARGIN = 20;
873 constexpr double BOX_WIDTH = 200;
874 constexpr double BOX_HEIGHT = 80;
875
876 double newX = 0;
877 const QMap<QString, QgsProcessingModelParameter> components = mModel->parameterComponents();
878 if ( !components.isEmpty() )
879 {
880 double maxX = 0;
881 for ( auto it = components.constBegin(); it != components.constEnd(); ++it )
882 {
883 maxX = std::max( maxX, it->position().x() );
884 }
885 newX = MARGIN + BOX_WIDTH + maxX;
886 }
887 else
888 {
889 newX = MARGIN + BOX_WIDTH / 2.0;
890 }
891 return QPointF( newX, MARGIN + BOX_HEIGHT / 2.0 );
892}
893
894QPointF QgsModelDesignerDialog::getPositionForAlgorithmItem() const
895{
896 constexpr double MARGIN = 20;
897 constexpr double BOX_WIDTH = 200;
898 constexpr double BOX_HEIGHT = 80;
899
900 const QMap<QString, QgsProcessingModelChildAlgorithm> algorithms = mModel->childAlgorithms();
901
902 double newX = 0;
903 double newY = 0;
904 if ( !algorithms.isEmpty() )
905 {
906 double maxX = 0;
907 double maxY = 0;
908 for ( auto it = algorithms.constBegin(); it != algorithms.constEnd(); ++it )
909 {
910 maxX = std::max( maxX, it->position().x() );
911 maxY = std::max( maxY, it->position().y() );
912 }
913 newX = MARGIN + BOX_WIDTH + maxX;
914 newY = MARGIN + BOX_HEIGHT + maxY;
915 }
916 else
917 {
918 newX = MARGIN + BOX_WIDTH / 2.0;
919 newY = MARGIN * 2 + BOX_HEIGHT + BOX_HEIGHT / 2.0;
920 }
921 return QPointF( newX, newY );
922}
923
924void QgsModelDesignerDialog::autoGenerateParameterName( QgsProcessingParameterDefinition *parameter ) const
925{
926 const QString safeName = QgsProcessingModelAlgorithm::safeName( parameter->description() );
927 QString name = safeName.toLower();
928 int i = 2;
929 while ( mModel->parameterDefinition( name ) )
930 {
931 name = safeName.toLower() + QString::number( i );
932 i += 1;
933 }
934 parameter->setName( name );
935}
936
937void QgsModelDesignerDialog::addAlgorithm( const QString &algorithmId, const QPointF &pos )
938{
939 std::unique_ptr< QgsProcessingAlgorithm > alg( QgsApplication::processingRegistry()->createAlgorithmById( algorithmId ) );
940 if ( !alg )
941 return;
942
943 QgsProcessingModelChildAlgorithm childAlg = QgsProcessingModelChildAlgorithm( algorithmId );
944 childAlg.setDescription( alg->displayName() );
945
946 if ( pos.isNull() )
947 {
948 childAlg.setPosition( getPositionForAlgorithmItem() );
949 }
950 else
951 {
952 childAlg.setPosition( pos );
953 }
954
955 childAlg.comment()->setPosition( childAlg.position() + QPointF( childAlg.size().width(), -1.5 * childAlg.size().height() ) );
956
957 const double outputOffsetX = childAlg.size().width();
958 double outputOffsetY = 1.5 * childAlg.size().height();
959
960 const QMap<QString, QgsProcessingModelOutput> childOutputs = childAlg.modelOutputs();
961 for ( auto out = childOutputs.constBegin(); out != childOutputs.constEnd(); ++out )
962 {
963 childAlg.modelOutput( out.key() ).setPosition( childAlg.position() + QPointF( outputOffsetX, outputOffsetY ) );
964 outputOffsetY += 1.5 * childAlg.modelOutput( out.key() ).size().height();
965 }
966
967 beginUndoCommand( tr( "Add Algorithm" ) );
968 mModel->addChildAlgorithm( childAlg );
969 repaintModel();
970 endUndoCommand();
971}
972
973void QgsModelDesignerDialog::addInput( const QString &parameterType, const QPointF &position )
974{
975 QPointF pos = position;
976 if ( !QgsApplication::processingRegistry()->parameterType( parameterType ) )
977 return;
978
979 std::unique_ptr< QgsProcessingParameterDefinition > newParam;
980 QString comment;
981
982 QgsProcessingParameterWidgetContext widgetContext = createWidgetContext();
983 QgsProcessingParameterDefinitionDialog dlg( parameterType, *mContext, widgetContext, nullptr, mModel.get() );
984 dlg.registerProcessingContextGenerator( this );
985 if ( !dlg.exec() )
986 return;
987
988 newParam.reset( dlg.createParameter() );
989 if ( !newParam )
990 return;
991
992 autoGenerateParameterName( newParam.get() );
993 dlg.comments();
994 comment = dlg.comments();
995
996 if ( pos.isNull() )
997 {
998 pos = getPositionForParameterItem();
999 }
1000
1001 QgsProcessingModelParameter component = QgsProcessingModelParameter( newParam->name() );
1002 component.setDescription( newParam->name() );
1003 component.setPosition( pos );
1004
1005 component.comment()->setDescription( comment );
1006 component.comment()->setPosition( component.position() + QPointF( component.size().width(), -1.5 * component.size().height() ) );
1007
1008 beginUndoCommand( tr( "Add Model Input" ) );
1009 mModel->addModelParameter( newParam.release(), component );
1010 repaintModel();
1011 endUndoCommand();
1012}
1013
1014QgsProcessingAlgorithmWidget *QgsModelDesignerDialog::createExecutionWidget()
1015{
1017 if ( !dialogFactory )
1018 return nullptr; // should never happen
1019
1020 QgsProcessingAlgorithmWidget *widget = dialogFactory->createWidget( mModel->create(), false, this, QgsProcessingAlgorithmWidgetBase::WidgetFlags(), Qgis::DockableWidgetInitialState::ForceDocked );
1021 if ( !widget )
1022 return nullptr; // should never happen
1023
1024 widget->registerProcessingFeedbackGenerator( this );
1025 return widget;
1026}
1027
1028void QgsModelDesignerDialog::exportAsScriptAlgorithm()
1029{
1031 if ( !dialogFactory )
1032 return; // should never happen
1033
1034 QgsProcessingScriptEditorDialog *dialog = dialogFactory->createScriptEditorDialog();
1035 if ( !dialog )
1036 return; // should never happen
1037
1038 const QStringList codeLines = mModel->asPythonCode( QgsProcessing::PythonOutputType::PythonQgsProcessingAlgorithmSubclass, 4 );
1039
1040 dialog->codeEditor()->setText( codeLines.join( '\n' ) );
1041
1042 dialog->show();
1043}
1044
1045void QgsModelDesignerDialog::zoomIn()
1046{
1047 mView->setTransformationAnchor( QGraphicsView::NoAnchor );
1048 QPointF point = mView->mapToScene( QPoint( mView->viewport()->width() / 2.0, mView->viewport()->height() / 2 ) );
1049 QgsSettings settings;
1050 const double factor = settings.value( u"/qgis/zoom_favor"_s, 2.0 ).toDouble();
1051 mView->scale( factor, factor );
1052 mView->centerOn( point );
1053}
1054
1055void QgsModelDesignerDialog::zoomOut()
1056{
1057 mView->setTransformationAnchor( QGraphicsView::NoAnchor );
1058 QPointF point = mView->mapToScene( QPoint( mView->viewport()->width() / 2.0, mView->viewport()->height() / 2 ) );
1059 QgsSettings settings;
1060 const double factor = 1.0 / settings.value( u"/qgis/zoom_favor"_s, 2.0 ).toDouble();
1061 mView->scale( factor, factor );
1062 mView->centerOn( point );
1063}
1064
1065void QgsModelDesignerDialog::zoomActual()
1066{
1067 QPointF point = mView->mapToScene( QPoint( mView->viewport()->width() / 2.0, mView->viewport()->height() / 2 ) );
1068 mView->resetTransform();
1069 mView->scale( mScreenHelper->screenDpi() / 96, mScreenHelper->screenDpi() / 96 );
1070 mView->centerOn( point );
1071}
1072
1073void QgsModelDesignerDialog::zoomFull()
1074{
1075 QRectF totalRect = mView->scene()->itemsBoundingRect();
1076 totalRect.adjust( -10, -10, 10, 10 );
1077 mView->fitInView( totalRect, Qt::KeepAspectRatio );
1078}
1079
1080void QgsModelDesignerDialog::newModel()
1081{
1082 if ( !checkForUnsavedChanges() )
1083 return;
1084
1085 auto alg = std::make_unique<QgsProcessingModelAlgorithm>();
1086 alg->setProvider( QgsApplication::processingRegistry()->providerById( QgsProcessing::MODEL_PROVIDER_ID ) );
1087 setModel( alg.release() );
1088}
1089
1090void QgsModelDesignerDialog::openModel()
1091{
1092 if ( !checkForUnsavedChanges() )
1093 return;
1094
1095 QgsSettings settings;
1096 const QString lastModelDir = settings.value( u"Processing/lastModelsDir"_s, QDir::homePath() ).toString();
1097 const QString fileName = QFileDialog::getOpenFileName( this, tr( "Open Model" ), lastModelDir, tr( "Processing models (*.model3 *.MODEL3)" ) );
1098 if ( !fileName.isEmpty() )
1099 {
1100 settings.setValue( u"Processing/lastModelsDir"_s, QFileInfo( fileName ).absoluteDir().absolutePath() );
1101 loadModel( fileName );
1102 }
1103}
1104
1105void QgsModelDesignerDialog::exportToImage()
1106{
1107 QgsSettings settings;
1108 QString lastExportDir = settings.value( u"lastModelDesignerExportDir"_s, QDir::homePath(), QgsSettings::App ).toString();
1109
1110 QString filename = QFileDialog::getSaveFileName( this, tr( "Save Model as Image" ), lastExportDir, tr( "PNG files (*.png *.PNG)" ) );
1111 // return dialog focus on Mac
1112 activateWindow();
1113 raise();
1114 if ( filename.isEmpty() )
1115 return;
1116
1117 filename = QgsFileUtils::ensureFileNameHasExtension( filename, QStringList() << u"png"_s );
1118
1119 const QFileInfo saveFileInfo( filename );
1120 settings.setValue( u"lastModelDesignerExportDir"_s, saveFileInfo.absolutePath(), QgsSettings::App );
1121
1122 repaintModel( false );
1123
1124 QRectF totalRect = mView->scene()->itemsBoundingRect();
1125 totalRect.adjust( -10, -10, 10, 10 );
1126 const QRectF imageRect = QRectF( 0, 0, totalRect.width(), totalRect.height() );
1127
1128 QImage img( totalRect.width(), totalRect.height(), QImage::Format_ARGB32_Premultiplied );
1129 img.fill( Qt::white );
1130 QPainter painter;
1131 painter.setRenderHint( QPainter::Antialiasing );
1132 painter.begin( &img );
1133 mView->scene()->render( &painter, imageRect, totalRect );
1134 painter.end();
1135
1136 img.save( filename );
1137
1138 mMessageBar
1139 ->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 );
1140 repaintModel( true );
1141}
1142
1143void QgsModelDesignerDialog::exportToPdf()
1144{
1145 QgsSettings settings;
1146 QString lastExportDir = settings.value( u"lastModelDesignerExportDir"_s, QDir::homePath(), QgsSettings::App ).toString();
1147
1148 QString filename = QFileDialog::getSaveFileName( this, tr( "Save Model as PDF" ), lastExportDir, tr( "PDF files (*.pdf *.PDF)" ) );
1149 // return dialog focus on Mac
1150 activateWindow();
1151 raise();
1152 if ( filename.isEmpty() )
1153 return;
1154
1155 filename = QgsFileUtils::ensureFileNameHasExtension( filename, QStringList() << u"pdf"_s );
1156
1157 const QFileInfo saveFileInfo( filename );
1158 settings.setValue( u"lastModelDesignerExportDir"_s, saveFileInfo.absolutePath(), QgsSettings::App );
1159
1160 repaintModel( false );
1161
1162 QRectF totalRect = mView->scene()->itemsBoundingRect();
1163 totalRect.adjust( -10, -10, 10, 10 );
1164 const QRectF printerRect = QRectF( 0, 0, totalRect.width(), totalRect.height() );
1165
1166 QPdfWriter pdfWriter( filename );
1167
1168 const double scaleFactor = 96 / 25.4; // based on 96 dpi sizes
1169
1170 QPageLayout pageLayout( QPageSize( totalRect.size() / scaleFactor, QPageSize::Millimeter ), QPageLayout::Portrait, QMarginsF( 0, 0, 0, 0 ) );
1171 pageLayout.setMode( QPageLayout::FullPageMode );
1172 pdfWriter.setPageLayout( pageLayout );
1173
1174 QPainter painter( &pdfWriter );
1175 mView->scene()->render( &painter, printerRect, totalRect );
1176 painter.end();
1177
1178 mMessageBar
1179 ->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 );
1180 repaintModel( true );
1181}
1182
1183void QgsModelDesignerDialog::exportToSvg()
1184{
1185 QgsSettings settings;
1186 QString lastExportDir = settings.value( u"lastModelDesignerExportDir"_s, QDir::homePath(), QgsSettings::App ).toString();
1187
1188 QString filename = QFileDialog::getSaveFileName( this, tr( "Save Model as SVG" ), lastExportDir, tr( "SVG files (*.svg *.SVG)" ) );
1189 // return dialog focus on Mac
1190 activateWindow();
1191 raise();
1192 if ( filename.isEmpty() )
1193 return;
1194
1195 filename = QgsFileUtils::ensureFileNameHasExtension( filename, QStringList() << u"svg"_s );
1196
1197 const QFileInfo saveFileInfo( filename );
1198 settings.setValue( u"lastModelDesignerExportDir"_s, saveFileInfo.absolutePath(), QgsSettings::App );
1199
1200 repaintModel( false );
1201
1202 QRectF totalRect = mView->scene()->itemsBoundingRect();
1203 totalRect.adjust( -10, -10, 10, 10 );
1204 const QRectF svgRect = QRectF( 0, 0, totalRect.width(), totalRect.height() );
1205
1206 QSvgGenerator svg;
1207 svg.setFileName( filename );
1208 svg.setSize( QSize( totalRect.width(), totalRect.height() ) );
1209 svg.setViewBox( svgRect );
1210 svg.setTitle( mModel->displayName() );
1211
1212 QPainter painter( &svg );
1213 mView->scene()->render( &painter, svgRect, totalRect );
1214 painter.end();
1215
1216 mMessageBar
1217 ->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 );
1218 repaintModel( true );
1219}
1220
1221void QgsModelDesignerDialog::exportAsPython()
1222{
1223 QgsSettings settings;
1224 QString lastExportDir = settings.value( u"lastModelDesignerExportDir"_s, QDir::homePath(), QgsSettings::App ).toString();
1225
1226 QString filename = QFileDialog::getSaveFileName( this, tr( "Save Model as Python Script" ), lastExportDir, tr( "Processing scripts (*.py *.PY)" ) );
1227 // return dialog focus on Mac
1228 activateWindow();
1229 raise();
1230 if ( filename.isEmpty() )
1231 return;
1232
1233 filename = QgsFileUtils::ensureFileNameHasExtension( filename, QStringList() << u"py"_s );
1234
1235 const QFileInfo saveFileInfo( filename );
1236 settings.setValue( u"lastModelDesignerExportDir"_s, saveFileInfo.absolutePath(), QgsSettings::App );
1237
1238 const QString text = mModel->asPythonCode( QgsProcessing::PythonOutputType::PythonQgsProcessingAlgorithmSubclass, 4 ).join( '\n' );
1239
1240 QFile outFile( filename );
1241 if ( !outFile.open( QIODevice::WriteOnly | QIODevice::Truncate ) )
1242 {
1243 return;
1244 }
1245 QTextStream fout( &outFile );
1246 fout << text;
1247 outFile.close();
1248
1249 mMessageBar
1250 ->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 );
1251}
1252
1253void QgsModelDesignerDialog::repaintModel( bool showControls )
1254{
1255 auto scene = new QgsModelGraphicsScene( this );
1256 if ( !showControls )
1257 {
1258 scene->setFlag( QgsModelGraphicsScene::Flag::FlagHideControls );
1259 }
1260
1261 const bool showComments = QgsSettings().value( "/Processing/Modeler/ShowComments", true ).toBool();
1262 if ( !showComments )
1263 {
1264 scene->setFlag( QgsModelGraphicsScene::Flag::FlagHideComments );
1265 }
1266
1267 setModelScene( scene );
1268 scene->createItems( mModel.get(), *mContext );
1269 scene->updateBounds();
1270}
1271
1272void QgsModelDesignerDialog::toggleComments( bool show )
1273{
1274 QgsSettings().setValue( u"/Processing/Modeler/ShowComments"_s, show );
1275
1276 repaintModel( true );
1277}
1278
1279void QgsModelDesignerDialog::toggleFeatureCount( bool show )
1280{
1281 QgsSettings().setValue( u"/Processing/Modeler/ShowFeatureCount"_s, show );
1282
1283 repaintModel( true );
1284}
1285
1286void QgsModelDesignerDialog::updateWindowTitle()
1287{
1288 QString title = tr( "Model Designer" );
1289 if ( !mModel->name().isEmpty() )
1290 title = mModel->group().isEmpty() ? u"%1: %2"_s.arg( title, mModel->name() ) : u"%1: %2 - %3"_s.arg( title, mModel->group(), mModel->name() );
1291
1292 if ( isDirty() )
1293 title.prepend( '*' );
1294
1295 setWindowTitle( title );
1296}
1297
1298void QgsModelDesignerDialog::deleteSelected()
1299{
1300 QList<QgsModelComponentGraphicItem *> items = mScene->selectedComponentItems();
1301 if ( items.empty() )
1302 return;
1303
1304 if ( items.size() == 1 )
1305 {
1306 items.at( 0 )->deleteComponent();
1307 return;
1308 }
1309
1310 std::sort( items.begin(), items.end(), []( QgsModelComponentGraphicItem *p1, QgsModelComponentGraphicItem *p2 ) {
1311 // try to delete the easy stuff first, so comments, then outputs, as nothing will depend on these...
1312 // NOLINTBEGIN(bugprone-branch-clone)
1313
1314 // 1. comments
1315 if ( dynamic_cast<QgsModelCommentGraphicItem *>( p1 ) && dynamic_cast<QgsModelCommentGraphicItem *>( p2 ) )
1316 return false;
1317 else if ( dynamic_cast<QgsModelCommentGraphicItem *>( p1 ) )
1318 return true;
1319 else if ( dynamic_cast<QgsModelCommentGraphicItem *>( p2 ) )
1320 return false;
1321 // 2. group boxes
1322 else if ( dynamic_cast<QgsModelGroupBoxGraphicItem *>( p1 ) && dynamic_cast<QgsModelGroupBoxGraphicItem *>( p2 ) )
1323 return false;
1324 else if ( dynamic_cast<QgsModelGroupBoxGraphicItem *>( p1 ) )
1325 return true;
1326 else if ( dynamic_cast<QgsModelGroupBoxGraphicItem *>( p2 ) )
1327 return false;
1328 // 3. outputs
1329 else if ( dynamic_cast<QgsModelOutputGraphicItem *>( p1 ) && dynamic_cast<QgsModelOutputGraphicItem *>( p2 ) )
1330 return false;
1331 else if ( dynamic_cast<QgsModelOutputGraphicItem *>( p1 ) )
1332 return true;
1333 else if ( dynamic_cast<QgsModelOutputGraphicItem *>( p2 ) )
1334 return false;
1335 // 4. child algorithms
1336 else if ( dynamic_cast<QgsModelChildAlgorithmGraphicItem *>( p1 ) && dynamic_cast<QgsModelChildAlgorithmGraphicItem *>( p2 ) )
1337 return false;
1338 else if ( dynamic_cast<QgsModelChildAlgorithmGraphicItem *>( p1 ) )
1339 return true;
1340 else if ( dynamic_cast<QgsModelChildAlgorithmGraphicItem *>( p2 ) )
1341 return false;
1342 return false;
1343 // NOLINTEND(bugprone-branch-clone)
1344 } );
1345
1346
1347 beginUndoCommand( tr( "Delete Components" ) );
1348
1349 QVariant prevState = mModel->toVariant();
1350 mBlockUndoCommands++;
1351 mBlockRepaints = true;
1352 bool failed = false;
1353 while ( !items.empty() )
1354 {
1355 QgsModelComponentGraphicItem *toDelete = nullptr;
1356 for ( QgsModelComponentGraphicItem *item : items )
1357 {
1358 if ( item->canDeleteComponent() )
1359 {
1360 toDelete = item;
1361 break;
1362 }
1363 }
1364
1365 if ( !toDelete )
1366 {
1367 failed = true;
1368 break;
1369 }
1370
1371 toDelete->deleteComponent();
1372 items.removeAll( toDelete );
1373 }
1374
1375 if ( failed )
1376 {
1377 mModel->loadVariant( prevState );
1378 QMessageBox::warning(
1379 nullptr,
1380 QObject::tr( "Could not remove components" ),
1381 QObject::tr(
1382 "Components depend on the selected items.\n"
1383 "Try to remove them before trying deleting these components."
1384 )
1385 );
1386 mBlockUndoCommands--;
1387 mActiveCommand.reset();
1388 }
1389 else
1390 {
1391 mBlockUndoCommands--;
1392 endUndoCommand();
1393 }
1394
1395 mBlockRepaints = false;
1396 repaintModel();
1397}
1398
1399void QgsModelDesignerDialog::populateZoomToMenu()
1400{
1401 mGroupMenu->clear();
1402 for ( const QgsProcessingModelGroupBox &box : model()->groupBoxes() )
1403 {
1404 if ( QgsModelComponentGraphicItem *item = mScene->groupBoxItem( box.uuid() ) )
1405 {
1406 QAction *zoomAction = new QAction( box.description(), mGroupMenu );
1407 connect( zoomAction, &QAction::triggered, this, [this, item] {
1408 QRectF groupRect = item->mapToScene( item->boundingRect() ).boundingRect();
1409 groupRect.adjust( -10, -10, 10, 10 );
1410 mView->fitInView( groupRect, Qt::KeepAspectRatio );
1411 mView->centerOn( item );
1412 } );
1413 mGroupMenu->addAction( zoomAction );
1414 }
1415 }
1416}
1417
1418void QgsModelDesignerDialog::setPanelVisibility( bool hidden )
1419{
1420 const QList<QDockWidget *> docks = findChildren<QDockWidget *>();
1421 const QList<QTabBar *> tabBars = findChildren<QTabBar *>();
1422
1423 if ( hidden )
1424 {
1425 mPanelStatus.clear();
1426 //record status of all docks
1427 for ( QDockWidget *dock : docks )
1428 {
1429 mPanelStatus.insert( dock->windowTitle(), PanelStatus( dock->isVisible(), false ) );
1430 dock->setVisible( false );
1431 }
1432
1433 //record active dock tabs
1434 for ( QTabBar *tabBar : tabBars )
1435 {
1436 QString currentTabTitle = tabBar->tabText( tabBar->currentIndex() );
1437 mPanelStatus[currentTabTitle].isActive = true;
1438 }
1439 }
1440 else
1441 {
1442 //restore visibility of all docks
1443 for ( QDockWidget *dock : docks )
1444 {
1445 if ( mPanelStatus.contains( dock->windowTitle() ) )
1446 {
1447 dock->setVisible( mPanelStatus.value( dock->windowTitle() ).isVisible );
1448 }
1449 }
1450
1451 //restore previously active dock tabs
1452 for ( QTabBar *tabBar : tabBars )
1453 {
1454 //loop through all tabs in tab bar
1455 for ( int i = 0; i < tabBar->count(); ++i )
1456 {
1457 QString tabTitle = tabBar->tabText( i );
1458 if ( mPanelStatus.contains( tabTitle ) && mPanelStatus.value( tabTitle ).isActive )
1459 {
1460 tabBar->setCurrentIndex( i );
1461 }
1462 }
1463 }
1464 mPanelStatus.clear();
1465 }
1466}
1467
1468void QgsModelDesignerDialog::editHelp()
1469{
1470 QgsProcessingHelpEditorDialog dialog( this );
1471 dialog.setWindowTitle( tr( "Edit Model Help" ) );
1472 dialog.setAlgorithm( mModel.get() );
1473 if ( dialog.exec() )
1474 {
1475 beginUndoCommand( tr( "Edit Model Help" ) );
1476 mModel->setHelpContent( dialog.helpContent() );
1477 endUndoCommand();
1478 }
1479}
1480
1481void QgsModelDesignerDialog::runSelectedSteps()
1482{
1483 QSet<QString> children;
1484 const QList<QgsModelComponentGraphicItem *> items = mScene->selectedComponentItems();
1485 for ( QgsModelComponentGraphicItem *item : items )
1486 {
1487 if ( QgsProcessingModelChildAlgorithm *childAlgorithm = dynamic_cast<QgsProcessingModelChildAlgorithm *>( item->component() ) )
1488 {
1489 children.insert( childAlgorithm->childId() );
1490 }
1491 }
1492
1493 if ( children.isEmpty() )
1494 {
1495 mMessageBar->pushWarning( QString(), tr( "No steps are selected" ) );
1496 return;
1497 }
1498
1499 run( children );
1500}
1501
1502void QgsModelDesignerDialog::runFromChild( const QString &id )
1503{
1504 QSet<QString> children = mModel->dependentChildAlgorithms( id );
1505 children.insert( id );
1506 run( children );
1507}
1508
1509void QgsModelDesignerDialog::cancelRunningModel()
1510{
1511 if ( !mAlgorithmWidget )
1512 return;
1513
1514 // these checks are wrong - mAlgorithmWidget is a QPointer, and we explicitly want to check
1515 // if it gets deleted in the cancel/forceClose dance!
1516 // cppcheck-suppress nullPointerRedundantCheck
1517 mAlgorithmWidget->cancel();
1518 // cppcheck-suppress nullPointerRedundantCheck
1519 mAlgorithmWidget->forceClose();
1520
1521 //Stop tracking change to the previous dialog in the QPointer
1522 if ( mAlgorithmWidget )
1523 {
1524 // this is a work around for the MESSY ownership issues associated with the python subclass
1525 // of QgsProcessingAlgorithmWidget. We have to FORCE all widgets to be deleted prior
1526 // to destruction of this window, and we can't be sure that python will have actually
1527 // deleted the widget when we asked...
1528 mAlgorithmWidgetsToCleanUp << mAlgorithmWidget;
1529 }
1530 mAlgorithmWidget.clear();
1531}
1532
1533void QgsModelDesignerDialog::run( const QSet<QString> &childAlgorithmSubset )
1534{
1535 QStringList errors;
1536 const bool isValid = model()->validate( errors );
1537 if ( !isValid )
1538 {
1539 QMessageBox messageBox;
1540 messageBox.setWindowTitle( tr( "Model is Invalid" ) );
1541 messageBox.setIcon( QMessageBox::Icon::Warning );
1542 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?" ) );
1543 messageBox.setStandardButtons( QMessageBox::StandardButton::Yes | QMessageBox::StandardButton::Cancel );
1544 messageBox.setDefaultButton( QMessageBox::StandardButton::Cancel );
1545
1546 QString errorString;
1547 for ( const QString &error : std::as_const( errors ) )
1548 {
1549 QString cleanedError = error;
1550 const thread_local QRegularExpression re( u"<[^>]*>"_s );
1551 cleanedError.replace( re, QString() );
1552 errorString += u"• %1\n"_s.arg( cleanedError );
1553 }
1554
1555 messageBox.setDetailedText( errorString );
1556 if ( messageBox.exec() == QMessageBox::StandardButton::Cancel )
1557 return;
1558 }
1559
1560 if ( !childAlgorithmSubset.isEmpty() )
1561 {
1562 for ( const QString &child : childAlgorithmSubset )
1563 {
1564 // has user previously run all requirements for this step?
1565 const QSet<QString> requirements = mModel->dependsOnChildAlgorithms( child );
1566 for ( const QString &requirement : requirements )
1567 {
1568 if ( !mLastResult.executedChildIds().contains( requirement ) )
1569 {
1570 QMessageBox messageBox;
1571 messageBox.setWindowTitle( tr( "Run Model" ) );
1572 messageBox.setIcon( QMessageBox::Icon::Warning );
1573 messageBox.setText( tr( "Prerequisite parts of this model have not yet been run (try running the full model first)." ) );
1574 messageBox.setStandardButtons( QMessageBox::StandardButton::Ok );
1575 messageBox.exec();
1576 return;
1577 }
1578 }
1579 }
1580 }
1581
1582 if ( mAlgorithmWidget && mAlgorithmWidget->isRunning() )
1583 {
1584 QMessageBox messageBox;
1585 messageBox.setIcon( QMessageBox::Icon::Warning );
1586 messageBox.setWindowTitle( tr( "Run Model" ) );
1587 messageBox.setText( tr( "This model is already running." ) );
1588 messageBox.setStandardButtons( QMessageBox::StandardButton::Cancel | QMessageBox::StandardButton::RestoreDefaults | QMessageBox::StandardButton::Ok );
1589
1590 QAbstractButton *buttonShowRunningAlg = messageBox.button( QMessageBox::StandardButton::Ok );
1591 buttonShowRunningAlg->setText( tr( "Show Progress" ) );
1592
1593 QAbstractButton *buttonReRun = messageBox.button( QMessageBox::StandardButton::RestoreDefaults );
1594 buttonReRun->setText( tr( "Cancel and Restart Model" ) );
1595
1596 int r = messageBox.exec();
1597
1598 switch ( r )
1599 {
1600 case QMessageBox::StandardButton::Cancel:
1601 return;
1602 case QMessageBox::StandardButton::RestoreDefaults:
1603 cancelRunningModel();
1604 break;
1605 case QMessageBox::StandardButton::Ok:
1606 mAlgorithmWidget->showWidget();
1607 return;
1608 default:
1609 break;
1610 }
1611 }
1612 else if ( mAlgorithmWidget )
1613 {
1614 // Close and create a new one
1615 mAlgorithmWidget->close();
1616 if ( mAlgorithmWidget )
1617 {
1618 // this is a work around for the MESSY ownership issues associated with the python subclass
1619 // of QgsProcessingAlgorithmWidget. We have to FORCE all widgets to be deleted prior
1620 // to destruction of this window, and we can't be sure that python will have actually
1621 // deleted the widget when we asked...
1622 mAlgorithmWidgetsToCleanUp << mAlgorithmWidget;
1623 }
1624 //Stop tracking change to the previous widget in the QPointer
1625 mAlgorithmWidget.clear();
1626 }
1627
1628 if ( !mAlgorithmWidget )
1629 {
1630 mAlgorithmWidget = createExecutionWidget();
1631 if ( !mAlgorithmWidget )
1632 return; // should not happen
1633
1634 mAlgorithmWidget->hideShortHelp();
1635 mAlgorithmWidget->setTitle( tr( "Run Model" ) );
1636
1637 mAlgorithmWidget->setLogLevel( Qgis::ProcessingLogLevel::ModelDebug );
1638 mAlgorithmWidget->setParameters( mModel->designerParameterValues() );
1639
1640 if ( !childAlgorithmSubset.isEmpty() )
1641 {
1642 mAlgorithmWidget->runButton()->setText( tr( "Run Subset" ) );
1643 mAlgorithmWidget->runButton()->setToolTip( tr( "Runs a subset of the child algorithms from this model" ) );
1644 }
1645
1646 connect( mAlgorithmWidget.get(), &QgsProcessingAlgorithmWidgetBase::algorithmAboutToRun, this, [this, childAlgorithmSubset]( QgsProcessingContext *context ) {
1647 if ( !childAlgorithmSubset.empty() )
1648 {
1649 // start from previous state
1650 auto modelConfig = std::make_unique<QgsProcessingModelInitialRunConfig>();
1651 modelConfig->setChildAlgorithmSubset( childAlgorithmSubset );
1652 modelConfig->setPreviouslyExecutedChildAlgorithms( mLastResult.executedChildIds() );
1653 modelConfig->setInitialChildInputs( mLastResult.rawChildInputs() );
1654 modelConfig->setInitialChildOutputs( mLastResult.rawChildOutputs() );
1655
1656 // add copies of layers from previous runs to context's layer store, so that they can be used
1657 // when running the subset
1658 const QMap<QString, QgsMapLayer *> previousOutputLayers = mLayerStore.temporaryLayerStore()->mapLayers();
1659 auto previousResultStore = std::make_unique<QgsMapLayerStore>();
1660 for ( auto it = previousOutputLayers.constBegin(); it != previousOutputLayers.constEnd(); ++it )
1661 {
1662 std::unique_ptr<QgsMapLayer> clone( it.value()->clone() );
1663 clone->setId( it.value()->id() );
1664 previousResultStore->addMapLayer( clone.release() );
1665 }
1666 previousResultStore->moveToThread( nullptr );
1667 modelConfig->setPreviousLayerStore( std::move( previousResultStore ) );
1668 context->setModelInitialRunConfig( std::move( modelConfig ) );
1669
1670 mScene->resetChildAlgorithmItems( childAlgorithmSubset );
1671
1672 // for all algorithms downstream of the subset which won't be re-run, flag their old results as outdated.
1673 for ( const QString &child : childAlgorithmSubset )
1674 {
1675 const QSet< QString > outdated = mModel->dependentChildAlgorithms( child );
1676 mScene->flagChildrenAsOutdated( outdated );
1677 mOutdatedChildResults.unite( outdated );
1678 }
1679 }
1680 else
1681 {
1682 // reset all child algorithm results
1683 mScene->resetChildAlgorithmItems();
1684 }
1685 } );
1686
1687 connect( mAlgorithmWidget, &QgsProcessingAlgorithmWidgetBase::algorithmFinished, this, [this]( bool, const QVariantMap &, QgsProcessingContext *context ) {
1688 // take child output layers
1689 mLayerStore.temporaryLayerStore()->removeAllMapLayers();
1690 mLayerStore.takeResultsFrom( *context );
1691
1692 mModel->setDesignerParameterValues( mAlgorithmWidget->createProcessingParameters( QgsProcessingParametersGenerator::Flag::SkipDefaultValueParameters ) );
1693 setLastRunResult( context->modelResult() );
1694 } );
1695 }
1696}
1697
1698void QgsModelDesignerDialog::showChildAlgorithmOutputs( const QString &childId )
1699{
1700 const bool isOutdated = mOutdatedChildResults.contains( childId );
1701 const QString childDescription = mModel->childAlgorithm( childId ).description();
1702
1703 const QgsProcessingModelChildAlgorithmResult result = mLastResult.childResults().value( childId );
1704 const QVariantMap childAlgorithmOutputs = result.outputs();
1705 if ( childAlgorithmOutputs.isEmpty() )
1706 {
1707 mMessageBar->pushWarning( QString(), tr( "No results are available for %1" ).arg( childDescription ) );
1708 return;
1709 }
1710
1711 const QgsProcessingAlgorithm *algorithm = mModel->childAlgorithm( childId ).algorithm();
1712 if ( !algorithm )
1713 {
1714 mMessageBar->pushCritical( QString(), tr( "Results cannot be shown for an invalid model component" ) );
1715 return;
1716 }
1717
1718 const QList<const QgsProcessingParameterDefinition *> outputParams = algorithm->destinationParameterDefinitions();
1719 if ( outputParams.isEmpty() )
1720 {
1721 // this situation should not arise in normal use, we don't show the action in this case
1722 QgsDebugError( "Cannot show results for algorithms with no outputs" );
1723 return;
1724 }
1725
1726 bool foundResults = false;
1727 for ( const QgsProcessingParameterDefinition *outputParam : outputParams )
1728 {
1729 const QVariant output = childAlgorithmOutputs.value( outputParam->name() );
1730 if ( !output.isValid() )
1731 continue;
1732
1733 if ( output.type() == QVariant::String )
1734 {
1735 if ( QgsMapLayer *resultLayer = QgsProcessingUtils::mapLayerFromString( output.toString(), mLayerStore ) )
1736 {
1737 QgsDebugMsgLevel( u"Loading previous result for %1: %2"_s.arg( outputParam->name(), output.toString() ), 2 );
1738
1739 std::unique_ptr<QgsMapLayer> layer( resultLayer->clone() );
1740
1741 QString baseName;
1742 if ( outputParams.size() > 1 )
1743 baseName = tr( "%1 — %2" ).arg( childDescription, outputParam->name() );
1744 else
1745 baseName = childDescription;
1746
1747 // make name unique, so that's it's easy to see which is the most recent result.
1748 // (this helps when running the model multiple times.)
1749 QString name = baseName;
1750 int counter = 1;
1751 while ( !QgsProject::instance()->mapLayersByName( name ).empty() )
1752 {
1753 counter += 1;
1754 name = tr( "%1 (%2)" ).arg( baseName ).arg( counter );
1755 }
1756
1757 layer->setName( name );
1758
1759 QgsProject::instance()->addMapLayer( layer.release() );
1760 foundResults = true;
1761 }
1762 else
1763 {
1764 // should not happen in normal operation
1765 QgsDebugError( u"Could not load previous result for %1: %2"_s.arg( outputParam->name(), output.toString() ) );
1766 }
1767 }
1768 }
1769
1770 if ( !foundResults )
1771 {
1772 mMessageBar->pushWarning( QString(), tr( "No results are available for %1" ).arg( childDescription ) );
1773 return;
1774 }
1775 else if ( isOutdated )
1776 {
1777 mMessageBar->pushWarning( QString(), tr( "These results are outdated, and may not reflect the most recent model execution" ) );
1778 return;
1779 }
1780}
1781
1782void QgsModelDesignerDialog::showChildAlgorithmLog( const QString &childId )
1783{
1784 const QString childDescription = mModel->childAlgorithm( childId ).description();
1785
1787 // prefer to fetch the log from the item itself -- if we are currently mid-way through
1788 // running the model, it will have the LATEST log available
1789 if ( QgsModelChildAlgorithmGraphicItem *item = mScene->childAlgorithmItem( childId ) )
1790 {
1791 result = item->results();
1792 }
1793 if ( result.htmlLog().isEmpty() )
1794 {
1795 result = mLastResult.childResults().value( childId );
1796 }
1797
1798 if ( result.htmlLog().isEmpty() )
1799 {
1800 mMessageBar->pushWarning( QString(), tr( "No log is available for %1" ).arg( childDescription ) );
1801 return;
1802 }
1803
1804 QgsMessageViewer m( this, QgsGuiUtils::ModalDialogFlags, false );
1805 m.setWindowTitle( childDescription );
1806 m.setCheckBoxVisible( false );
1807 m.setMessageAsHtml( result.htmlLog() );
1808 m.exec();
1809}
1810
1811void QgsModelDesignerDialog::onItemFocused( QgsModelComponentGraphicItem *item )
1812{
1813 QgsProcessingParameterWidgetContext widgetContext = createWidgetContext();
1814 widgetContext.registerProcessingContextGenerator( this );
1815 widgetContext.setModelDesignerDialog( this );
1816
1817 if ( !item || !item->component() )
1818 {
1819 mConfigWidget->showComponentConfig( nullptr, *mContext, widgetContext );
1820 }
1821 else
1822 {
1823 mConfigWidget->showComponentConfig( item->component(), *mContext, widgetContext );
1824
1825 if ( auto childAlgorithmItem = qobject_cast< QgsModelChildAlgorithmGraphicItem * >( item ) )
1826 {
1827 connect( childAlgorithmItem, &QgsModelChildAlgorithmGraphicItem::rebuildConfigurationDockWidget, childAlgorithmItem, [this] {
1828 QgsProcessingParameterWidgetContext widgetContext = createWidgetContext();
1829 widgetContext.registerProcessingContextGenerator( this );
1830 widgetContext.setModelDesignerDialog( this );
1831 mConfigWidget->showComponentConfig( nullptr, *mContext, widgetContext );
1832 } );
1833 }
1834 }
1835}
1836
1837void QgsModelDesignerDialog::validate()
1838{
1839 QStringList issues;
1840 if ( model()->validate( issues ) )
1841 {
1842 mMessageBar->pushSuccess( QString(), tr( "Model is valid!" ) );
1843 }
1844 else
1845 {
1846 QgsMessageBarItem *messageWidget = QgsMessageBar::createMessage( QString(), tr( "Model is invalid!" ) );
1847 QPushButton *detailsButton = new QPushButton( tr( "Details" ) );
1848 connect( detailsButton, &QPushButton::clicked, detailsButton, [detailsButton, issues] {
1849 QgsMessageViewer *dialog = new QgsMessageViewer( detailsButton );
1850 dialog->setTitle( tr( "Model is Invalid" ) );
1851
1852 QString longMessage = tr( "<p>This model is not valid:</p>" ) + u"<ul>"_s;
1853 for ( const QString &issue : issues )
1854 {
1855 longMessage += u"<li>%1</li>"_s.arg( issue );
1856 }
1857 longMessage += "</ul>"_L1;
1858
1859 dialog->setMessage( longMessage, Qgis::StringFormat::Html );
1860 dialog->showMessage();
1861 } );
1862 messageWidget->layout()->addWidget( detailsButton );
1863 mMessageBar->clearWidgets();
1864 mMessageBar->pushWidget( messageWidget, Qgis::MessageLevel::Warning, 0 );
1865 }
1866}
1867
1868void QgsModelDesignerDialog::reorderInputs()
1869{
1870 QgsModelInputReorderDialog dlg( this );
1871 dlg.setModel( mModel.get() );
1872 if ( dlg.exec() )
1873 {
1874 const QStringList inputOrder = dlg.inputOrder();
1875 beginUndoCommand( tr( "Reorder Inputs" ) );
1876 mModel->setParameterOrder( inputOrder );
1877 endUndoCommand();
1878 }
1879}
1880
1881void QgsModelDesignerDialog::reorderOutputs()
1882{
1883 QgsModelOutputReorderDialog dlg( this );
1884 dlg.setModel( mModel.get() );
1885 if ( dlg.exec() )
1886 {
1887 const QStringList outputOrder = dlg.outputOrder();
1888 beginUndoCommand( tr( "Reorder Outputs" ) );
1889 mModel->setOutputOrder( outputOrder );
1890 mModel->setOutputGroup( dlg.outputGroup() );
1891 endUndoCommand();
1892 }
1893}
1894
1895bool QgsModelDesignerDialog::isDirty() const
1896{
1897 return mHasChanged && mUndoStack->index() != -1;
1898}
1899
1900void QgsModelDesignerDialog::fillInputsTree()
1901{
1902 const QIcon icon = QgsApplication::getThemeIcon( u"mIconModelInput.svg"_s );
1903 auto parametersItem = std::make_unique<QTreeWidgetItem>();
1904 parametersItem->setText( 0, tr( "Parameters" ) );
1905 QList<QgsProcessingParameterType *> available = QgsApplication::processingRegistry()->parameterTypes();
1906 std::sort( available.begin(), available.end(), []( const QgsProcessingParameterType *a, const QgsProcessingParameterType *b ) -> bool {
1907 return QString::localeAwareCompare( a->name(), b->name() ) < 0;
1908 } );
1909
1910 for ( QgsProcessingParameterType *param : std::as_const( available ) )
1911 {
1913 {
1914 auto paramItem = std::make_unique<QTreeWidgetItem>();
1915 paramItem->setText( 0, param->name() );
1916 paramItem->setData( 0, Qt::UserRole, param->id() );
1917 paramItem->setIcon( 0, icon );
1918 paramItem->setFlags( Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled );
1919 paramItem->setToolTip( 0, param->description() );
1920 parametersItem->addChild( paramItem.release() );
1921 }
1922 }
1923 mInputsTreeWidget->addTopLevelItem( parametersItem.release() );
1924 mInputsTreeWidget->topLevelItem( 0 )->setExpanded( true );
1925}
1926
1927
1928//
1929// QgsModelChildDependenciesWidget
1930//
1931
1932QgsModelChildDependenciesWidget::QgsModelChildDependenciesWidget( QWidget *parent, QgsProcessingModelAlgorithm *model, const QString &childId )
1933 : QWidget( parent )
1934 , mModel( model )
1935 , mChildId( childId )
1936{
1937 QHBoxLayout *hl = new QHBoxLayout();
1938 hl->setContentsMargins( 0, 0, 0, 0 );
1939
1940 mLineEdit = new QLineEdit();
1941 mLineEdit->setEnabled( false );
1942 hl->addWidget( mLineEdit, 1 );
1943
1944 mToolButton = new QToolButton();
1945 mToolButton->setText( QString( QChar( 0x2026 ) ) );
1946 hl->addWidget( mToolButton );
1947
1948 setLayout( hl );
1949
1950 mLineEdit->setText( tr( "%1 dependencies selected" ).arg( 0 ) );
1951
1952 connect( mToolButton, &QToolButton::clicked, this, &QgsModelChildDependenciesWidget::showDialog );
1953}
1954
1955void QgsModelChildDependenciesWidget::setValue( const QList<QgsProcessingModelChildDependency> &value )
1956{
1957 const bool hasChanged = value != mValue;
1958 mValue = value;
1959
1960 updateSummaryText();
1961 if ( hasChanged )
1962 {
1963 emit changed();
1964 }
1965}
1966
1967void QgsModelChildDependenciesWidget::showDialog()
1968{
1969 const QList<QgsProcessingModelChildDependency> available = mModel->availableDependenciesForChildAlgorithm( mChildId );
1970
1971 QVariantList availableOptions;
1972 for ( const QgsProcessingModelChildDependency &dep : available )
1973 availableOptions << QVariant::fromValue( dep );
1974 QVariantList selectedOptions;
1975 for ( const QgsProcessingModelChildDependency &dep : mValue )
1976 selectedOptions << QVariant::fromValue( dep );
1977
1979 if ( panel )
1980 {
1981 QgsProcessingMultipleSelectionPanelWidget *widget = new QgsProcessingMultipleSelectionPanelWidget( availableOptions, selectedOptions );
1982 widget->setPanelTitle( tr( "Algorithm Dependencies" ) );
1983
1984 widget->setValueFormatter( [this]( const QVariant &v ) -> QString {
1985 const QgsProcessingModelChildDependency dep = v.value<QgsProcessingModelChildDependency>();
1986
1987 const QString description = mModel->childAlgorithm( dep.childId ).description();
1988 if ( dep.conditionalBranch.isEmpty() )
1989 return description;
1990 else
1991 return tr( "Condition “%1” from algorithm “%2”" ).arg( dep.conditionalBranch, description );
1992 } );
1993
1994 connect( widget, &QgsProcessingMultipleSelectionPanelWidget::selectionChanged, this, [this, widget]() {
1995 QList<QgsProcessingModelChildDependency> res;
1996 for ( const QVariant &v : widget->selectedOptions() )
1997 {
1998 res << v.value<QgsProcessingModelChildDependency>();
1999 }
2000 setValue( res );
2001 } );
2002 connect( widget, &QgsProcessingMultipleSelectionPanelWidget::acceptClicked, widget, &QgsPanelWidget::acceptPanel );
2003 panel->openPanel( widget );
2004 }
2005}
2006
2007void QgsModelChildDependenciesWidget::updateSummaryText()
2008{
2009 mLineEdit->setText( tr( "%n dependencies selected", nullptr, mValue.count() ) );
2010}
2011
static QString version()
Version string.
Definition qgis.cpp:682
@ ExposeToModeler
Is this parameter available in the modeler. Is set to on by default.
Definition qgis.h:4000
@ Warning
Warning message.
Definition qgis.h:162
@ Critical
Critical/error message.
Definition qgis.h:163
@ Success
Used for reporting a successful operation.
Definition qgis.h:164
@ Html
HTML message.
Definition qgis.h:177
@ ForceDocked
Force the widget to be docked.
Definition qgis.h:7105
@ ModelDebug
Model debug level logging. Includes verbose logging and other outputs useful for debugging models.
Definition qgis.h:3898
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.
A singleton class containing various registry and other global members related to GUI classes.
Definition qgsgui.h:71
static QSize iconSize(Qgis::UserInterfaceIconType iconType=Qgis::UserInterfaceIconType::MainWindowToolbar)
Returns the desired size for user interface icons.
Definition qgsgui.cpp:514
static QgsProcessingGuiRegistry * processingGuiRegistry()
Returns the global processing gui registry, used for registering the GUI behavior of processing algor...
Definition qgsgui.cpp:170
void applicationStyleSheetChanged(const QString &styleSheet)
Emitted whenever the application style sheet is changed.
static QString applicationStyleSheet()
Returns the application style sheet.
Definition qgsgui.cpp:399
static QgsGui * instance()
Returns a pointer to the singleton instance.
Definition qgsgui.cpp:94
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:225
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(), Qgis::StringFormat format=Qgis::StringFormat::PlainText)
Adds a message to the log instance (and creates it if necessary).
A generic message view for displaying QGIS messages.
void setMessage(const QString &message, Qgis::StringFormat format) override
Sets message, it won't be displayed until.
void setTitle(const QString &title) override
Sets title for the messages.
void showMessage(bool blocking=true) override
display the message to the user and deletes itself
A dockable panel widget stack which allows users to specify the properties of a Processing model comp...
Model designer view tool for panning a 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.
QgsProcessingModelResult modelResult() const
Returns the model results, populated when the context is used to run a model algorithm.
A factory for creating widgets and dialogs for Processing components.
virtual QgsProcessingScriptEditorDialog * createScriptEditorDialog(const QString &filePath=QString(), QWidget *parent=nullptr)=0
Creates a new script editor dialog.
virtual QgsProcessingAlgorithmWidget * createWidget(QgsProcessingAlgorithm *algorithm, bool inPlace=false, QMainWindow *parent=nullptr, QgsProcessingAlgorithmWidgetBase::WidgetFlags flags=QgsProcessingAlgorithmWidgetBase::WidgetFlags(), Qgis::DockableWidgetInitialState initialState=Qgis::DockableWidgetInitialState::RestorePreviousState)=0
Creates a new widget for executing the specified algorithm.
Base class for providing feedback from a processing algorithm.
QgsProcessingDialogFactory * dialogFactory()
Returns the application's processing dialog factory, if set.
QgsProcessingParameterWidgetContext createWidgetContext() override
Register a Processing widget context.
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.
void childResultReported(const QString &childId, const QgsProcessingModelChildAlgorithmResult &result)
Emitted when the result of a child algorithm has been reported.
Encapsulates the results of running a Processing model.
QMap< QString, QgsProcessingModelChildAlgorithmResult > childResults() const
Returns the map of child algorithm results.
A dialog which allows users to specify the properties of a Processing parameter.
Base class for the definition of processing parameters.
QString description() const
Returns the description for the parameter.
void setName(const QString &name)
Sets the name of the parameter.
Makes metadata of processing parameters available.
Contains settings which reflect the context in which a Processing parameter widget is shown.
void setModelDesignerDialog(QgsModelDesignerDialog *dialog)
Sets the associated model designer dialog, if applicable.
void setModel(QgsProcessingModelAlgorithm *model)
Sets the model which the parameter widget is associated with.
void registerProcessingContextGenerator(QgsProcessingContextGenerator *generator)
Registers a Processing context generator class that will be used to retrieve a Processing context for...
@ SkipDefaultValueParameters
Parameters which are unchanged from their default values should not be included.
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 QStringList modelFolders()
Returns the list of locations for reading Processing model files.
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.
static const QString MODEL_PROVIDER_ID
Constant used for the model provider's ID.
@ PythonQgsProcessingAlgorithmSubclass
Full Python QgsProcessingAlgorithm subclass.
static const QString PROJECT_PROVIDER_ID
Constant used for the project model provider's ID.
Describes the version of a project.
QString text() const
Returns a string representation of the version.
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.
void setDirty(bool b=true)
Flag the project as dirty (modified).
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.
void addDockWidget(QMainWindow *window, Qt::DockWidgetArea area, QDockWidget *dockwidget)
Add a dock widget to a main window.
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:80
#define QgsDebugError(str)
Definition qgslogger.h:71
constexpr QObjectUniquePtr< Tp > make_qobject_unique(Args &&...args)
Create an object owned by a QObjectUniquePtr.