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