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