QGIS API Documentation 3.99.0-Master (c22de0620c0)
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}
74
75Qt::ItemFlags QgsModelerToolboxModel::flags( const QModelIndex &index ) const
76{
77 Qt::ItemFlags f = QgsProcessingToolboxProxyModel::flags( index );
78 const QModelIndex sourceIndex = mapToSource( index );
79 if ( toolboxModel()->isAlgorithm( sourceIndex ) || toolboxModel()->isParameter( sourceIndex ) )
80 {
81 f = f | Qt::ItemIsDragEnabled;
82 }
83 return f;
84}
85
86Qt::DropActions QgsModelerToolboxModel::supportedDragActions() const
87{
88 return Qt::CopyAction;
89}
90
91QgsModelDesignerDialog::QgsModelDesignerDialog( QWidget *parent, Qt::WindowFlags flags )
92 : QMainWindow( parent, flags )
93 , mToolsActionGroup( new QActionGroup( this ) )
94{
95 setupUi( this );
96
97 mLayerStore.setProject( QgsProject::instance() );
98
99 mScreenHelper = new QgsScreenHelper( this );
100
101 setAttribute( Qt::WA_DeleteOnClose );
102 setDockOptions( dockOptions() | QMainWindow::GroupedDragging );
103 setWindowFlags( Qt::WindowMinimizeButtonHint | Qt::WindowMaximizeButtonHint | Qt::WindowCloseButtonHint );
104
106
107 mModel = std::make_unique<QgsProcessingModelAlgorithm>();
108 mModel->setProvider( QgsApplication::processingRegistry()->providerById( u"model"_s ) );
109
110 mUndoStack = new QUndoStack( this );
111 connect( mUndoStack, &QUndoStack::indexChanged, this, [this] {
112 if ( mIgnoreUndoStackChanges )
113 return;
114
115 mBlockUndoCommands++;
116 updateVariablesGui();
117 mGroupEdit->setText( mModel->group() );
118 mNameEdit->setText( mModel->displayName() );
119 mBlockUndoCommands--;
120 repaintModel();
121 } );
122
123 mConfigWidgetDock = new QgsDockWidget( this );
124 mConfigWidgetDock->setWindowTitle( tr( "Configuration" ) );
125 mConfigWidgetDock->setObjectName( u"ModelConfigDock"_s );
126
127 mConfigWidget = new QgsModelDesignerConfigDockWidget();
128 mConfigWidgetDock->setWidget( mConfigWidget );
129 mConfigWidgetDock->setFeatures( QDockWidget::NoDockWidgetFeatures );
130 addDockWidget( Qt::RightDockWidgetArea, mConfigWidgetDock );
131
132 mPropertiesDock->setFeatures( QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetClosable );
133 mInputsDock->setFeatures( QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetClosable );
134 mAlgorithmsDock->setFeatures( QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetClosable );
135 mVariablesDock->setFeatures( QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetClosable );
136
137 mToolboxTree->header()->setVisible( false );
138 mToolboxSearchEdit->setShowSearchIcon( true );
139 mToolboxSearchEdit->setPlaceholderText( tr( "Search…" ) );
140 connect( mToolboxSearchEdit, &QgsFilterLineEdit::textChanged, mToolboxTree, &QgsProcessingToolboxTreeView::setFilterString );
141
142 mInputsTreeWidget->header()->setVisible( false );
143 mInputsTreeWidget->setAlternatingRowColors( true );
144 mInputsTreeWidget->setDragDropMode( QTreeWidget::DragOnly );
145 mInputsTreeWidget->setDropIndicatorShown( true );
146
147 mNameEdit->setPlaceholderText( tr( "Enter model name here" ) );
148 mGroupEdit->setPlaceholderText( tr( "Enter group name here" ) );
149
150 mMessageBar = new QgsMessageBar();
151 mMessageBar->setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Fixed );
152 mainLayout->insertWidget( 0, mMessageBar );
153
154 mView->setAcceptDrops( true );
155 QgsSettings settings;
156
157 connect( mActionClose, &QAction::triggered, this, &QWidget::close );
158 connect( mActionNew, &QAction::triggered, this, &QgsModelDesignerDialog::newModel );
159 connect( mActionZoomIn, &QAction::triggered, this, &QgsModelDesignerDialog::zoomIn );
160 connect( mActionZoomOut, &QAction::triggered, this, &QgsModelDesignerDialog::zoomOut );
161 connect( mActionZoomActual, &QAction::triggered, this, &QgsModelDesignerDialog::zoomActual );
162 connect( mActionZoomToItems, &QAction::triggered, this, &QgsModelDesignerDialog::zoomFull );
163 connect( mActionExportImage, &QAction::triggered, this, &QgsModelDesignerDialog::exportToImage );
164 connect( mActionExportPdf, &QAction::triggered, this, &QgsModelDesignerDialog::exportToPdf );
165 connect( mActionExportSvg, &QAction::triggered, this, &QgsModelDesignerDialog::exportToSvg );
166 connect( mActionExportPython, &QAction::triggered, this, &QgsModelDesignerDialog::exportAsPython );
167 connect( mActionSave, &QAction::triggered, this, [this] { saveModel( false ); } );
168 connect( mActionSaveAs, &QAction::triggered, this, [this] { saveModel( true ); } );
169 connect( mActionDeleteComponents, &QAction::triggered, this, &QgsModelDesignerDialog::deleteSelected );
170 connect( mActionSnapSelected, &QAction::triggered, mView, &QgsModelGraphicsView::snapSelected );
171 connect( mActionValidate, &QAction::triggered, this, &QgsModelDesignerDialog::validate );
172 connect( mActionReorderInputs, &QAction::triggered, this, &QgsModelDesignerDialog::reorderInputs );
173 connect( mActionReorderOutputs, &QAction::triggered, this, &QgsModelDesignerDialog::reorderOutputs );
174 connect( mActionEditHelp, &QAction::triggered, this, &QgsModelDesignerDialog::editHelp );
175 connect( mReorderInputsButton, &QPushButton::clicked, this, &QgsModelDesignerDialog::reorderInputs );
176 connect( mActionRun, &QAction::triggered, this, [this] { run(); } );
177 connect( mActionRunSelectedSteps, &QAction::triggered, this, &QgsModelDesignerDialog::runSelectedSteps );
178
179 mActionSnappingEnabled->setChecked( settings.value( u"/Processing/Modeler/enableSnapToGrid"_s, false ).toBool() );
180 connect( mActionSnappingEnabled, &QAction::toggled, this, [this]( bool enabled ) {
181 mView->snapper()->setSnapToGrid( enabled );
182 QgsSettings().setValue( u"/Processing/Modeler/enableSnapToGrid"_s, enabled );
183 } );
184 mView->snapper()->setSnapToGrid( mActionSnappingEnabled->isChecked() );
185
186 connect( mView, &QgsModelGraphicsView::itemFocused, this, &QgsModelDesignerDialog::onItemFocused );
187
188 connect( mActionSelectAll, &QAction::triggered, this, [this] {
189 mScene->selectAll();
190 } );
191
192 QStringList docksTitle = settings.value( u"ModelDesigner/hiddenDocksTitle"_s, QStringList(), QgsSettings::App ).toStringList();
193 QStringList docksActive = settings.value( u"ModelDesigner/hiddenDocksActive"_s, QStringList(), QgsSettings::App ).toStringList();
194 if ( !docksTitle.isEmpty() )
195 {
196 for ( const auto &title : docksTitle )
197 {
198 mPanelStatus.insert( title, PanelStatus( true, docksActive.contains( title ) ) );
199 }
200 }
201 mActionHidePanels->setChecked( !docksTitle.isEmpty() );
202 connect( mActionHidePanels, &QAction::toggled, this, &QgsModelDesignerDialog::setPanelVisibility );
203
204 mUndoAction = mUndoStack->createUndoAction( this );
205 mUndoAction->setIcon( QgsApplication::getThemeIcon( u"/mActionUndo.svg"_s ) );
206 mUndoAction->setShortcuts( QKeySequence::Undo );
207 mRedoAction = mUndoStack->createRedoAction( this );
208 mRedoAction->setIcon( QgsApplication::getThemeIcon( u"/mActionRedo.svg"_s ) );
209 mRedoAction->setShortcuts( QKeySequence::Redo );
210
211 mMenuEdit->insertAction( mActionDeleteComponents, mRedoAction );
212 mMenuEdit->insertAction( mActionDeleteComponents, mUndoAction );
213 mMenuEdit->insertSeparator( mActionDeleteComponents );
214 mToolbar->insertAction( mActionZoomIn, mUndoAction );
215 mToolbar->insertAction( mActionZoomIn, mRedoAction );
216 mToolbar->insertSeparator( mActionZoomIn );
217
218 mGroupMenu = new QMenu( tr( "Zoom To" ), this );
219 mMenuView->insertMenu( mActionZoomIn, mGroupMenu );
220 connect( mGroupMenu, &QMenu::aboutToShow, this, &QgsModelDesignerDialog::populateZoomToMenu );
221
222 //cut/copy/paste actions. Note these are not included in the ui file
223 //as ui files have no support for QKeySequence shortcuts
224 mActionCut = new QAction( tr( "Cu&t" ), this );
225 mActionCut->setShortcuts( QKeySequence::Cut );
226 mActionCut->setStatusTip( tr( "Cut" ) );
227 mActionCut->setIcon( QgsApplication::getThemeIcon( u"/mActionEditCut.svg"_s ) );
228 connect( mActionCut, &QAction::triggered, this, [this] {
229 mView->copySelectedItems( QgsModelGraphicsView::ClipboardCut );
230 } );
231
232 mActionCopy = new QAction( tr( "&Copy" ), this );
233 mActionCopy->setShortcuts( QKeySequence::Copy );
234 mActionCopy->setStatusTip( tr( "Copy" ) );
235 mActionCopy->setIcon( QgsApplication::getThemeIcon( u"/mActionEditCopy.svg"_s ) );
236 connect( mActionCopy, &QAction::triggered, this, [this] {
237 mView->copySelectedItems( QgsModelGraphicsView::ClipboardCopy );
238 } );
239
240 mActionPaste = new QAction( tr( "&Paste" ), this );
241 mActionPaste->setShortcuts( QKeySequence::Paste );
242 mActionPaste->setStatusTip( tr( "Paste" ) );
243 mActionPaste->setIcon( QgsApplication::getThemeIcon( u"/mActionEditPaste.svg"_s ) );
244 connect( mActionPaste, &QAction::triggered, this, [this] {
245 mView->pasteItems( QgsModelGraphicsView::PasteModeCursor );
246 } );
247 mMenuEdit->insertAction( mActionDeleteComponents, mActionCut );
248 mMenuEdit->insertAction( mActionDeleteComponents, mActionCopy );
249 mMenuEdit->insertAction( mActionDeleteComponents, mActionPaste );
250 mMenuEdit->insertSeparator( mActionDeleteComponents );
251
252 mAlgorithmsModel = new QgsModelerToolboxModel( this );
253 mToolboxTree->setToolboxProxyModel( mAlgorithmsModel );
254
256 if ( settings.value( u"Processing/Configuration/SHOW_ALGORITHMS_KNOWN_ISSUES"_s, false ).toBool() )
257 {
259 }
260 mToolboxTree->setFilters( filters );
261 mToolboxTree->setDragDropMode( QTreeWidget::DragOnly );
262 mToolboxTree->setDropIndicatorShown( true );
263
264 connect( mView, &QgsModelGraphicsView::algorithmDropped, this, [this]( const QString &algorithmId, const QPointF &pos ) {
265 addAlgorithm( algorithmId, pos );
266 } );
267 connect( mView, &QgsModelGraphicsView::inputDropped, this, &QgsModelDesignerDialog::addInput );
268
269 connect( mToolboxTree, &QgsProcessingToolboxTreeView::doubleClicked, this, [this]( const QModelIndex & ) {
270 if ( mToolboxTree->selectedAlgorithm() )
271 addAlgorithm( mToolboxTree->selectedAlgorithm()->id(), QPointF() );
272 if ( mToolboxTree->selectedParameterType() )
273 addInput( mToolboxTree->selectedParameterType()->id(), QPointF() );
274 } );
275
276 connect( mInputsTreeWidget, &QgsModelDesignerInputsTreeWidget::doubleClicked, this, [this]( const QModelIndex & ) {
277 const QString parameterType = mInputsTreeWidget->currentItem()->data( 0, Qt::UserRole ).toString();
278 addInput( parameterType, QPointF() );
279 } );
280
281 // Ctrl+= should also trigger a zoom in action
282 QShortcut *ctrlEquals = new QShortcut( QKeySequence( u"Ctrl+="_s ), this );
283 connect( ctrlEquals, &QShortcut::activated, this, &QgsModelDesignerDialog::zoomIn );
284
285 mUndoDock = new QgsDockWidget( tr( "Undo History" ), this );
286 mUndoDock->setObjectName( u"UndoDock"_s );
287 mUndoView = new QUndoView( mUndoStack, this );
288 mUndoDock->setWidget( mUndoView );
289 mUndoDock->setFeatures( QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetClosable );
290 addDockWidget( Qt::DockWidgetArea::LeftDockWidgetArea, mUndoDock );
291
292 tabifyDockWidget( mUndoDock, mPropertiesDock );
293 tabifyDockWidget( mVariablesDock, mPropertiesDock );
294 mPropertiesDock->raise();
295 tabifyDockWidget( mInputsDock, mAlgorithmsDock );
296 mInputsDock->raise();
297
298 connect( mVariablesEditor, &QgsVariableEditorWidget::scopeChanged, this, [this] {
299 if ( mModel )
300 {
301 beginUndoCommand( tr( "Change Model Variables" ) );
302 mModel->setVariables( mVariablesEditor->variablesInActiveScope() );
303 endUndoCommand();
304 }
305 } );
306 connect( mNameEdit, &QLineEdit::textChanged, this, [this]( const QString &name ) {
307 if ( mModel )
308 {
309 beginUndoCommand( tr( "Change Model Name" ), QString(), QgsModelUndoCommand::CommandOperation::NameChanged );
310 mModel->setName( name );
311 endUndoCommand();
312 updateWindowTitle();
313 }
314 } );
315 connect( mGroupEdit, &QLineEdit::textChanged, this, [this]( const QString &group ) {
316 if ( mModel )
317 {
318 beginUndoCommand( tr( "Change Model Group" ), QString(), QgsModelUndoCommand::CommandOperation::GroupChanged );
319 mModel->setGroup( group );
320 endUndoCommand();
321 updateWindowTitle();
322 }
323 } );
324
325 fillInputsTree();
326
327 QToolButton *toolbuttonExportToScript = new QToolButton();
328 toolbuttonExportToScript->setPopupMode( QToolButton::InstantPopup );
329 toolbuttonExportToScript->addAction( mActionExportAsScriptAlgorithm );
330 toolbuttonExportToScript->setDefaultAction( mActionExportAsScriptAlgorithm );
331 mToolbar->insertWidget( mActionExportImage, toolbuttonExportToScript );
332 connect( mActionExportAsScriptAlgorithm, &QAction::triggered, this, &QgsModelDesignerDialog::exportAsScriptAlgorithm );
333
334 mActionShowComments->setChecked( settings.value( u"/Processing/Modeler/ShowComments"_s, true ).toBool() );
335 connect( mActionShowComments, &QAction::toggled, this, &QgsModelDesignerDialog::toggleComments );
336
337 mActionShowFeatureCount->setChecked( settings.value( u"/Processing/Modeler/ShowFeatureCount"_s, true ).toBool() );
338 connect( mActionShowFeatureCount, &QAction::toggled, this, &QgsModelDesignerDialog::toggleFeatureCount );
339
340 mPanTool = new QgsModelViewToolPan( mView );
341 mPanTool->setAction( mActionPan );
342
343 mToolsActionGroup->addAction( mActionPan );
344 connect( mActionPan, &QAction::triggered, mPanTool, [this] { mView->setTool( mPanTool ); } );
345
346 mSelectTool = new QgsModelViewToolSelect( mView );
347 mSelectTool->setAction( mActionSelectMoveItem );
348
349 mToolsActionGroup->addAction( mActionSelectMoveItem );
350 connect( mActionSelectMoveItem, &QAction::triggered, mSelectTool, [this] { mView->setTool( mSelectTool ); } );
351
352 mView->setTool( mSelectTool );
353 mView->setFocus();
354
355 connect( mView, &QgsModelGraphicsView::macroCommandStarted, this, [this]( const QString &text ) {
356 mIgnoreUndoStackChanges++;
357 mUndoStack->beginMacro( text );
358 mIgnoreUndoStackChanges--;
359 } );
360 connect( mView, &QgsModelGraphicsView::macroCommandEnded, this, [this] {
361 mIgnoreUndoStackChanges++;
362 mUndoStack->endMacro();
363 mIgnoreUndoStackChanges--;
364 } );
365 connect( mView, &QgsModelGraphicsView::commandBegun, this, [this]( const QString &text ) {
366 beginUndoCommand( text );
367 } );
368 connect( mView, &QgsModelGraphicsView::commandEnded, this, [this] {
369 endUndoCommand();
370 } );
371 connect( mView, &QgsModelGraphicsView::commandAborted, this, [this] {
372 abortUndoCommand();
373 } );
374 connect( mView, &QgsModelGraphicsView::deleteSelectedItems, this, [this] {
375 deleteSelected();
376 } );
377
378 connect( mActionAddGroupBox, &QAction::triggered, this, [this] {
379 const QPointF viewCenter = mView->mapToScene( mView->viewport()->rect().center() );
380 QgsProcessingModelGroupBox group;
381 group.setPosition( viewCenter );
382 group.setDescription( tr( "New Group" ) );
383
384 beginUndoCommand( tr( "Add Group Box" ) );
385 model()->addGroupBox( group );
386 repaintModel();
387 endUndoCommand();
388 } );
389
390 updateWindowTitle();
391
392 // restore the toolbar and dock widgets positions using Qt settings API
393 restoreState( settings.value( u"ModelDesigner/state"_s, QByteArray(), QgsSettings::App ).toByteArray() );
394}
395
396QgsModelDesignerDialog::~QgsModelDesignerDialog()
397{
398 QgsSettings settings;
399 if ( !mPanelStatus.isEmpty() )
400 {
401 QStringList docksTitle;
402 QStringList docksActive;
403
404 for ( const auto &panel : mPanelStatus.toStdMap() )
405 {
406 if ( panel.second.isVisible )
407 docksTitle << panel.first;
408 if ( panel.second.isActive )
409 docksActive << panel.first;
410 }
411 settings.setValue( u"ModelDesigner/hiddenDocksTitle"_s, docksTitle, QgsSettings::App );
412 settings.setValue( u"ModelDesigner/hiddenDocksActive"_s, docksActive, QgsSettings::App );
413 }
414 else
415 {
416 settings.remove( u"ModelDesigner/hiddenDocksTitle"_s, QgsSettings::App );
417 settings.remove( u"ModelDesigner/hiddenDocksActive"_s, QgsSettings::App );
418 }
419
420 // store the toolbar/dock widget settings using Qt settings API
421 settings.setValue( u"ModelDesigner/state"_s, saveState(), QgsSettings::App );
422
423 mIgnoreUndoStackChanges++;
424 delete mSelectTool; // delete mouse handles before everything else
425}
426
427void QgsModelDesignerDialog::closeEvent( QCloseEvent *event )
428{
429 if ( checkForUnsavedChanges() )
430 event->accept();
431 else
432 event->ignore();
433}
434
435void QgsModelDesignerDialog::beginUndoCommand( const QString &text, const QString &id, QgsModelUndoCommand::CommandOperation operation )
436{
437 if ( mBlockUndoCommands || !mUndoStack )
438 return;
439
440 if ( mActiveCommand )
441 endUndoCommand();
442
443 if ( !id.isEmpty() )
444 {
445 mActiveCommand = std::make_unique<QgsModelUndoCommand>( mModel.get(), text, id );
446 }
447 else
448 {
449 mActiveCommand = std::make_unique<QgsModelUndoCommand>( mModel.get(), text, operation );
450 }
451}
452
453void QgsModelDesignerDialog::endUndoCommand()
454{
455 if ( mBlockUndoCommands || !mActiveCommand || !mUndoStack )
456 return;
457
458 mActiveCommand->saveAfterState();
459 mIgnoreUndoStackChanges++;
460 mUndoStack->push( mActiveCommand.release() );
461 mIgnoreUndoStackChanges--;
462 setDirty( true );
463}
464
465void QgsModelDesignerDialog::abortUndoCommand()
466{
467 if ( mActiveCommand )
468 mActiveCommand->setObsolete( true );
469}
470
471QgsProcessingModelAlgorithm *QgsModelDesignerDialog::model()
472{
473 return mModel.get();
474}
475
476void QgsModelDesignerDialog::setModel( QgsProcessingModelAlgorithm *model )
477{
478 mModel.reset( model );
479
480 mGroupEdit->setText( mModel->group() );
481 mNameEdit->setText( mModel->displayName() );
482 repaintModel( true );
483 updateVariablesGui();
484
485 mView->centerOn( 0, 0 );
486 setDirty( false );
487
488 mIgnoreUndoStackChanges++;
489 mUndoStack->clear();
490 mIgnoreUndoStackChanges--;
491
492 updateWindowTitle();
493}
494
495void QgsModelDesignerDialog::loadModel( const QString &path )
496{
497 auto alg = std::make_unique<QgsProcessingModelAlgorithm>();
498 if ( alg->fromFile( path ) )
499 {
500 alg->setProvider( QgsApplication::processingRegistry()->providerById( u"model"_s ) );
501 alg->setSourceFilePath( path );
502 setModel( alg.release() );
503 }
504 else
505 {
506 QgsMessageLog::logMessage( tr( "Could not load model %1" ).arg( path ), tr( "Processing" ), Qgis::MessageLevel::Critical );
507 QMessageBox::critical( this, tr( "Open Model" ), tr( "The selected model could not be loaded.\n"
508 "See the log for more information." ) );
509 }
510}
511
512void QgsModelDesignerDialog::setModelScene( QgsModelGraphicsScene *scene )
513{
514 QgsModelGraphicsScene *oldScene = mScene;
515
516 mScene = scene;
517 mScene->setParent( this );
518 mScene->setLastRunResult( mLastResult, mLayerStore );
519 mScene->setModel( mModel.get() );
520 mScene->setMessageBar( mMessageBar );
521
522 QgsSettings settings;
523 const bool showFeatureCount = settings.value( u"/Processing/Modeler/ShowFeatureCount"_s, true ).toBool();
524 if ( !showFeatureCount )
525 mScene->setFlag( QgsModelGraphicsScene::FlagHideFeatureCount );
526
527 mView->setModelScene( mScene );
528
529 mSelectTool->resetCache();
530 mSelectTool->setScene( mScene );
531
532 connect( mScene, &QgsModelGraphicsScene::rebuildRequired, this, [this] {
533 if ( mBlockRepaints )
534 return;
535
536 repaintModel();
537 } );
538 connect( mScene, &QgsModelGraphicsScene::componentAboutToChange, this, [this]( const QString &description, const QString &id ) { beginUndoCommand( description, id ); } );
539 connect( mScene, &QgsModelGraphicsScene::componentChanged, this, [this] { endUndoCommand(); } );
540 connect( mScene, &QgsModelGraphicsScene::runFromChild, this, &QgsModelDesignerDialog::runFromChild );
541 connect( mScene, &QgsModelGraphicsScene::runSelected, this, &QgsModelDesignerDialog::runSelectedSteps );
542 connect( mScene, &QgsModelGraphicsScene::showChildAlgorithmOutputs, this, &QgsModelDesignerDialog::showChildAlgorithmOutputs );
543 connect( mScene, &QgsModelGraphicsScene::showChildAlgorithmLog, this, &QgsModelDesignerDialog::showChildAlgorithmLog );
544
545 if ( oldScene )
546 oldScene->deleteLater();
547}
548
549QgsModelGraphicsScene *QgsModelDesignerDialog::modelScene()
550{
551 return mScene;
552}
553
554void QgsModelDesignerDialog::activate()
555{
556 show();
557 raise();
558 setWindowState( windowState() & ~Qt::WindowMinimized );
559 activateWindow();
560}
561
562void QgsModelDesignerDialog::registerProcessingContextGenerator( QgsProcessingContextGenerator *generator )
563{
564 mProcessingContextGenerator = generator;
565}
566
567void QgsModelDesignerDialog::updateVariablesGui()
568{
569 mBlockUndoCommands++;
570
571 auto variablesScope = std::make_unique<QgsExpressionContextScope>( tr( "Model Variables" ) );
572 const QVariantMap modelVars = mModel->variables();
573 for ( auto it = modelVars.constBegin(); it != modelVars.constEnd(); ++it )
574 {
575 variablesScope->setVariable( it.key(), it.value() );
576 }
577 QgsExpressionContext variablesContext;
578 variablesContext.appendScope( variablesScope.release() );
579 mVariablesEditor->setContext( &variablesContext );
580 mVariablesEditor->setEditableScopeIndex( 0 );
581
582 mBlockUndoCommands--;
583}
584
585void QgsModelDesignerDialog::setDirty( bool dirty )
586{
587 mHasChanged = dirty;
588 updateWindowTitle();
589}
590
591bool QgsModelDesignerDialog::validateSave( SaveAction action )
592{
593 switch ( action )
594 {
595 case QgsModelDesignerDialog::SaveAction::SaveAsFile:
596 break;
597 case QgsModelDesignerDialog::SaveAction::SaveInProject:
598 if ( mNameEdit->text().trimmed().isEmpty() )
599 {
600 mMessageBar->pushWarning( QString(), tr( "Please enter a model name before saving" ) );
601 return false;
602 }
603 break;
604 }
605
606 return true;
607}
608
609bool QgsModelDesignerDialog::checkForUnsavedChanges()
610{
611 if ( isDirty() )
612 {
613 QMessageBox::StandardButton ret = QMessageBox::question( this, tr( "Save Model?" ), tr( "There are unsaved changes in this model. Do you want to keep those?" ), QMessageBox::Save | QMessageBox::Cancel | QMessageBox::Discard, QMessageBox::Cancel );
614 switch ( ret )
615 {
616 case QMessageBox::Save:
617 return saveModel( false );
618
619 case QMessageBox::Discard:
620 return true;
621
622 default:
623 return false;
624 }
625 }
626 else
627 {
628 return true;
629 }
630}
631
632void QgsModelDesignerDialog::setLastRunResult( const QgsProcessingModelResult &result )
633{
634 mLastResult.mergeWith( result );
635 if ( mScene )
636 mScene->setLastRunResult( mLastResult, mLayerStore );
637}
638
639void QgsModelDesignerDialog::setModelName( const QString &name )
640{
641 mNameEdit->setText( name );
642}
643
644void QgsModelDesignerDialog::zoomIn()
645{
646 mView->setTransformationAnchor( QGraphicsView::NoAnchor );
647 QPointF point = mView->mapToScene( QPoint( mView->viewport()->width() / 2.0, mView->viewport()->height() / 2 ) );
648 QgsSettings settings;
649 const double factor = settings.value( u"/qgis/zoom_favor"_s, 2.0 ).toDouble();
650 mView->scale( factor, factor );
651 mView->centerOn( point );
652}
653
654void QgsModelDesignerDialog::zoomOut()
655{
656 mView->setTransformationAnchor( QGraphicsView::NoAnchor );
657 QPointF point = mView->mapToScene( QPoint( mView->viewport()->width() / 2.0, mView->viewport()->height() / 2 ) );
658 QgsSettings settings;
659 const double factor = 1.0 / settings.value( u"/qgis/zoom_favor"_s, 2.0 ).toDouble();
660 mView->scale( factor, factor );
661 mView->centerOn( point );
662}
663
664void QgsModelDesignerDialog::zoomActual()
665{
666 QPointF point = mView->mapToScene( QPoint( mView->viewport()->width() / 2.0, mView->viewport()->height() / 2 ) );
667 mView->resetTransform();
668 mView->scale( mScreenHelper->screenDpi() / 96, mScreenHelper->screenDpi() / 96 );
669 mView->centerOn( point );
670}
671
672void QgsModelDesignerDialog::zoomFull()
673{
674 QRectF totalRect = mView->scene()->itemsBoundingRect();
675 totalRect.adjust( -10, -10, 10, 10 );
676 mView->fitInView( totalRect, Qt::KeepAspectRatio );
677}
678
679void QgsModelDesignerDialog::newModel()
680{
681 if ( !checkForUnsavedChanges() )
682 return;
683
684 auto alg = std::make_unique<QgsProcessingModelAlgorithm>();
685 alg->setProvider( QgsApplication::processingRegistry()->providerById( u"model"_s ) );
686 setModel( alg.release() );
687}
688
689void QgsModelDesignerDialog::exportToImage()
690{
691 QgsSettings settings;
692 QString lastExportDir = settings.value( u"lastModelDesignerExportDir"_s, QDir::homePath(), QgsSettings::App ).toString();
693
694 QString filename = QFileDialog::getSaveFileName( this, tr( "Save Model as Image" ), lastExportDir, tr( "PNG files (*.png *.PNG)" ) );
695 // return dialog focus on Mac
696 activateWindow();
697 raise();
698 if ( filename.isEmpty() )
699 return;
700
701 filename = QgsFileUtils::ensureFileNameHasExtension( filename, QStringList() << u"png"_s );
702
703 const QFileInfo saveFileInfo( filename );
704 settings.setValue( u"lastModelDesignerExportDir"_s, saveFileInfo.absolutePath(), QgsSettings::App );
705
706 repaintModel( false );
707
708 QRectF totalRect = mView->scene()->itemsBoundingRect();
709 totalRect.adjust( -10, -10, 10, 10 );
710 const QRectF imageRect = QRectF( 0, 0, totalRect.width(), totalRect.height() );
711
712 QImage img( totalRect.width(), totalRect.height(), QImage::Format_ARGB32_Premultiplied );
713 img.fill( Qt::white );
714 QPainter painter;
715 painter.setRenderHint( QPainter::Antialiasing );
716 painter.begin( &img );
717 mView->scene()->render( &painter, imageRect, totalRect );
718 painter.end();
719
720 img.save( filename );
721
722 mMessageBar->pushMessage( QString(), tr( "Successfully exported model as image to <a href=\"%1\">%2</a>" ).arg( QUrl::fromLocalFile( filename ).toString(), QDir::toNativeSeparators( filename ) ), Qgis::MessageLevel::Success, 0 );
723 repaintModel( true );
724}
725
726void QgsModelDesignerDialog::exportToPdf()
727{
728 QgsSettings settings;
729 QString lastExportDir = settings.value( u"lastModelDesignerExportDir"_s, QDir::homePath(), QgsSettings::App ).toString();
730
731 QString filename = QFileDialog::getSaveFileName( this, tr( "Save Model as PDF" ), lastExportDir, tr( "PDF files (*.pdf *.PDF)" ) );
732 // return dialog focus on Mac
733 activateWindow();
734 raise();
735 if ( filename.isEmpty() )
736 return;
737
738 filename = QgsFileUtils::ensureFileNameHasExtension( filename, QStringList() << u"pdf"_s );
739
740 const QFileInfo saveFileInfo( filename );
741 settings.setValue( u"lastModelDesignerExportDir"_s, saveFileInfo.absolutePath(), QgsSettings::App );
742
743 repaintModel( false );
744
745 QRectF totalRect = mView->scene()->itemsBoundingRect();
746 totalRect.adjust( -10, -10, 10, 10 );
747 const QRectF printerRect = QRectF( 0, 0, totalRect.width(), totalRect.height() );
748
749 QPdfWriter pdfWriter( filename );
750
751 const double scaleFactor = 96 / 25.4; // based on 96 dpi sizes
752
753 QPageLayout pageLayout( QPageSize( totalRect.size() / scaleFactor, QPageSize::Millimeter ), QPageLayout::Portrait, QMarginsF( 0, 0, 0, 0 ) );
754 pageLayout.setMode( QPageLayout::FullPageMode );
755 pdfWriter.setPageLayout( pageLayout );
756
757 QPainter painter( &pdfWriter );
758 mView->scene()->render( &painter, printerRect, totalRect );
759 painter.end();
760
761 mMessageBar->pushMessage( QString(), tr( "Successfully exported model as PDF to <a href=\"%1\">%2</a>" ).arg( QUrl::fromLocalFile( filename ).toString(), QDir::toNativeSeparators( filename ) ), Qgis::MessageLevel::Success, 0 );
762 repaintModel( true );
763}
764
765void QgsModelDesignerDialog::exportToSvg()
766{
767 QgsSettings settings;
768 QString lastExportDir = settings.value( u"lastModelDesignerExportDir"_s, QDir::homePath(), QgsSettings::App ).toString();
769
770 QString filename = QFileDialog::getSaveFileName( this, tr( "Save Model as SVG" ), lastExportDir, tr( "SVG files (*.svg *.SVG)" ) );
771 // return dialog focus on Mac
772 activateWindow();
773 raise();
774 if ( filename.isEmpty() )
775 return;
776
777 filename = QgsFileUtils::ensureFileNameHasExtension( filename, QStringList() << u"svg"_s );
778
779 const QFileInfo saveFileInfo( filename );
780 settings.setValue( u"lastModelDesignerExportDir"_s, saveFileInfo.absolutePath(), QgsSettings::App );
781
782 repaintModel( false );
783
784 QRectF totalRect = mView->scene()->itemsBoundingRect();
785 totalRect.adjust( -10, -10, 10, 10 );
786 const QRectF svgRect = QRectF( 0, 0, totalRect.width(), totalRect.height() );
787
788 QSvgGenerator svg;
789 svg.setFileName( filename );
790 svg.setSize( QSize( totalRect.width(), totalRect.height() ) );
791 svg.setViewBox( svgRect );
792 svg.setTitle( mModel->displayName() );
793
794 QPainter painter( &svg );
795 mView->scene()->render( &painter, svgRect, totalRect );
796 painter.end();
797
798 mMessageBar->pushMessage( QString(), tr( "Successfully exported model as SVG to <a href=\"%1\">%2</a>" ).arg( QUrl::fromLocalFile( filename ).toString(), QDir::toNativeSeparators( filename ) ), Qgis::MessageLevel::Success, 0 );
799 repaintModel( true );
800}
801
802void QgsModelDesignerDialog::exportAsPython()
803{
804 QgsSettings settings;
805 QString lastExportDir = settings.value( u"lastModelDesignerExportDir"_s, QDir::homePath(), QgsSettings::App ).toString();
806
807 QString filename = QFileDialog::getSaveFileName( this, tr( "Save Model as Python Script" ), lastExportDir, tr( "Processing scripts (*.py *.PY)" ) );
808 // return dialog focus on Mac
809 activateWindow();
810 raise();
811 if ( filename.isEmpty() )
812 return;
813
814 filename = QgsFileUtils::ensureFileNameHasExtension( filename, QStringList() << u"py"_s );
815
816 const QFileInfo saveFileInfo( filename );
817 settings.setValue( u"lastModelDesignerExportDir"_s, saveFileInfo.absolutePath(), QgsSettings::App );
818
819 const QString text = mModel->asPythonCode( QgsProcessing::PythonOutputType::PythonQgsProcessingAlgorithmSubclass, 4 ).join( '\n' );
820
821 QFile outFile( filename );
822 if ( !outFile.open( QIODevice::WriteOnly | QIODevice::Truncate ) )
823 {
824 return;
825 }
826 QTextStream fout( &outFile );
827 fout << text;
828 outFile.close();
829
830 mMessageBar->pushMessage( QString(), tr( "Successfully exported model as Python script to <a href=\"%1\">%2</a>" ).arg( QUrl::fromLocalFile( filename ).toString(), QDir::toNativeSeparators( filename ) ), Qgis::MessageLevel::Success, 0 );
831}
832
833void QgsModelDesignerDialog::toggleComments( bool show )
834{
835 QgsSettings().setValue( u"/Processing/Modeler/ShowComments"_s, show );
836
837 repaintModel( true );
838}
839
840void QgsModelDesignerDialog::toggleFeatureCount( bool show )
841{
842 QgsSettings().setValue( u"/Processing/Modeler/ShowFeatureCount"_s, show );
843
844 repaintModel( true );
845}
846
847void QgsModelDesignerDialog::updateWindowTitle()
848{
849 QString title = tr( "Model Designer" );
850 if ( !mModel->name().isEmpty() )
851 title = mModel->group().isEmpty()
852 ? u"%1: %2"_s.arg( title, mModel->name() )
853 : u"%1: %2 - %3"_s.arg( title, mModel->group(), mModel->name() );
854
855 if ( isDirty() )
856 title.prepend( '*' );
857
858 setWindowTitle( title );
859}
860
861void QgsModelDesignerDialog::deleteSelected()
862{
863 QList<QgsModelComponentGraphicItem *> items = mScene->selectedComponentItems();
864 if ( items.empty() )
865 return;
866
867 if ( items.size() == 1 )
868 {
869 items.at( 0 )->deleteComponent();
870 return;
871 }
872
873 std::sort( items.begin(), items.end(), []( QgsModelComponentGraphicItem *p1, QgsModelComponentGraphicItem *p2 ) {
874 // try to delete the easy stuff first, so comments, then outputs, as nothing will depend on these...
875 // NOLINTBEGIN(bugprone-branch-clone)
876
877 // 1. comments
878 if ( dynamic_cast<QgsModelCommentGraphicItem *>( p1 ) && dynamic_cast<QgsModelCommentGraphicItem *>( p2 ) )
879 return false;
880 else if ( dynamic_cast<QgsModelCommentGraphicItem *>( p1 ) )
881 return true;
882 else if ( dynamic_cast<QgsModelCommentGraphicItem *>( p2 ) )
883 return false;
884 // 2. group boxes
885 else if ( dynamic_cast<QgsModelGroupBoxGraphicItem *>( p1 ) && dynamic_cast<QgsModelGroupBoxGraphicItem *>( p2 ) )
886 return false;
887 else if ( dynamic_cast<QgsModelGroupBoxGraphicItem *>( p1 ) )
888 return true;
889 else if ( dynamic_cast<QgsModelGroupBoxGraphicItem *>( p2 ) )
890 return false;
891 // 3. outputs
892 else if ( dynamic_cast<QgsModelOutputGraphicItem *>( p1 ) && dynamic_cast<QgsModelOutputGraphicItem *>( p2 ) )
893 return false;
894 else if ( dynamic_cast<QgsModelOutputGraphicItem *>( p1 ) )
895 return true;
896 else if ( dynamic_cast<QgsModelOutputGraphicItem *>( p2 ) )
897 return false;
898 // 4. child algorithms
899 else if ( dynamic_cast<QgsModelChildAlgorithmGraphicItem *>( p1 ) && dynamic_cast<QgsModelChildAlgorithmGraphicItem *>( p2 ) )
900 return false;
901 else if ( dynamic_cast<QgsModelChildAlgorithmGraphicItem *>( p1 ) )
902 return true;
903 else if ( dynamic_cast<QgsModelChildAlgorithmGraphicItem *>( p2 ) )
904 return false;
905 return false;
906 // NOLINTEND(bugprone-branch-clone)
907 } );
908
909
910 beginUndoCommand( tr( "Delete Components" ) );
911
912 QVariant prevState = mModel->toVariant();
913 mBlockUndoCommands++;
914 mBlockRepaints = true;
915 bool failed = false;
916 while ( !items.empty() )
917 {
918 QgsModelComponentGraphicItem *toDelete = nullptr;
919 for ( QgsModelComponentGraphicItem *item : items )
920 {
921 if ( item->canDeleteComponent() )
922 {
923 toDelete = item;
924 break;
925 }
926 }
927
928 if ( !toDelete )
929 {
930 failed = true;
931 break;
932 }
933
934 toDelete->deleteComponent();
935 items.removeAll( toDelete );
936 }
937
938 if ( failed )
939 {
940 mModel->loadVariant( prevState );
941 QMessageBox::warning( nullptr, QObject::tr( "Could not remove components" ), QObject::tr( "Components depend on the selected items.\n"
942 "Try to remove them before trying deleting these components." ) );
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:3823
@ Warning
Warning message.
Definition qgis.h:160
@ Critical
Critical/error message.
Definition qgis.h:161
@ Success
Used for reporting a successful operation.
Definition qgis.h:162
@ Html
HTML message.
Definition qgis.h:175
@ ModelDebug
Model debug level logging. Includes verbose logging and other outputs useful for debugging models.
Definition qgis.h:3745
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