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