QGIS API Documentation  3.16.0-Hannover (43b64b13f3)
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 
16 #include "qgsmodeldesignerdialog.h"
17 #include "qgssettings.h"
18 #include "qgsapplication.h"
19 #include "qgsfileutils.h"
20 #include "qgsmessagebar.h"
21 #include "qgsprocessingmodelalgorithm.h"
22 #include "qgsprocessingregistry.h"
23 #include "qgsprocessingalgorithm.h"
24 #include "qgsgui.h"
26 #include "qgsmodelundocommand.h"
27 #include "qgsmodelviewtoolselect.h"
28 #include "qgsmodelgraphicsscene.h"
30 #include "processing/models/qgsprocessingmodelgroupbox.h"
32 #include "qgsmessageviewer.h"
33 #include "qgsmessagebaritem.h"
34 #include "qgspanelwidget.h"
36 
37 #include <QShortcut>
38 #include <QDesktopWidget>
39 #include <QKeySequence>
40 #include <QFileDialog>
41 #include <QPrinter>
42 #include <QSvgGenerator>
43 #include <QToolButton>
44 #include <QCloseEvent>
45 #include <QMessageBox>
46 #include <QUndoView>
47 #include <QPushButton>
48 
50 
51 
52 QgsModelerToolboxModel::QgsModelerToolboxModel( QObject *parent )
54 {
55 
56 }
57 
58 Qt::ItemFlags QgsModelerToolboxModel::flags( const QModelIndex &index ) const
59 {
60  Qt::ItemFlags f = QgsProcessingToolboxProxyModel::flags( index );
61  const QModelIndex sourceIndex = mapToSource( index );
62  if ( toolboxModel()->isAlgorithm( sourceIndex ) )
63  {
64  f = f | Qt::ItemIsDragEnabled;
65  }
66  return f;
67 }
68 
69 Qt::DropActions QgsModelerToolboxModel::supportedDragActions() const
70 {
71  return Qt::CopyAction;
72 }
73 
74 
75 
76 QgsModelDesignerDialog::QgsModelDesignerDialog( QWidget *parent, Qt::WindowFlags flags )
77  : QMainWindow( parent, flags )
78 {
79  setupUi( this );
81 
82  setAttribute( Qt::WA_DeleteOnClose );
83  setDockOptions( dockOptions() | QMainWindow::GroupedDragging );
84  setWindowFlags( Qt::WindowMinimizeButtonHint |
85  Qt::WindowMaximizeButtonHint |
86  Qt::WindowCloseButtonHint );
87 
88  mModel = qgis::make_unique< QgsProcessingModelAlgorithm >();
89  mModel->setProvider( QgsApplication::processingRegistry()->providerById( QStringLiteral( "model" ) ) );
90 
91  mUndoStack = new QUndoStack( this );
92  connect( mUndoStack, &QUndoStack::indexChanged, this, [ = ]
93  {
94  if ( mIgnoreUndoStackChanges )
95  return;
96 
97  mBlockUndoCommands++;
98  updateVariablesGui();
99  mGroupEdit->setText( mModel->group() );
100  mNameEdit->setText( mModel->displayName() );
101  mBlockUndoCommands--;
102  repaintModel();
103  } );
104 
105  mPropertiesDock->setFeatures( QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetMovable );
106  mInputsDock->setFeatures( QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetMovable );
107  mAlgorithmsDock->setFeatures( QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetMovable );
108  mVariablesDock->setFeatures( QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetClosable );
109 
110  mAlgorithmsTree->header()->setVisible( false );
111  mAlgorithmSearchEdit->setShowSearchIcon( true );
112  mAlgorithmSearchEdit->setPlaceholderText( tr( "Search…" ) );
113  connect( mAlgorithmSearchEdit, &QgsFilterLineEdit::textChanged, mAlgorithmsTree, &QgsProcessingToolboxTreeView::setFilterString );
114 
115  mInputsTreeWidget->header()->setVisible( false );
116  mInputsTreeWidget->setAlternatingRowColors( true );
117  mInputsTreeWidget->setDragDropMode( QTreeWidget::DragOnly );
118  mInputsTreeWidget->setDropIndicatorShown( true );
119 
120  mNameEdit->setPlaceholderText( tr( "Enter model name here" ) );
121  mGroupEdit->setPlaceholderText( tr( "Enter group name here" ) );
122 
123  mMessageBar = new QgsMessageBar();
124  mMessageBar->setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Fixed );
125  mainLayout->insertWidget( 0, mMessageBar );
126 
127  mView->setAcceptDrops( true );
128  QgsSettings settings;
129 
130  connect( mActionClose, &QAction::triggered, this, &QWidget::close );
131  connect( mActionZoomIn, &QAction::triggered, this, &QgsModelDesignerDialog::zoomIn );
132  connect( mActionZoomOut, &QAction::triggered, this, &QgsModelDesignerDialog::zoomOut );
133  connect( mActionZoomActual, &QAction::triggered, this, &QgsModelDesignerDialog::zoomActual );
134  connect( mActionZoomToItems, &QAction::triggered, this, &QgsModelDesignerDialog::zoomFull );
135  connect( mActionExportImage, &QAction::triggered, this, &QgsModelDesignerDialog::exportToImage );
136  connect( mActionExportPdf, &QAction::triggered, this, &QgsModelDesignerDialog::exportToPdf );
137  connect( mActionExportSvg, &QAction::triggered, this, &QgsModelDesignerDialog::exportToSvg );
138  connect( mActionExportPython, &QAction::triggered, this, &QgsModelDesignerDialog::exportAsPython );
139  connect( mActionSave, &QAction::triggered, this, [ = ] { saveModel( false ); } );
140  connect( mActionSaveAs, &QAction::triggered, this, [ = ] { saveModel( true ); } );
141  connect( mActionDeleteComponents, &QAction::triggered, this, &QgsModelDesignerDialog::deleteSelected );
142  connect( mActionSnapSelected, &QAction::triggered, mView, &QgsModelGraphicsView::snapSelected );
143  connect( mActionValidate, &QAction::triggered, this, &QgsModelDesignerDialog::validate );
144  connect( mActionReorderInputs, &QAction::triggered, this, &QgsModelDesignerDialog::reorderInputs );
145  connect( mReorderInputsButton, &QPushButton::clicked, this, &QgsModelDesignerDialog::reorderInputs );
146 
147  mActionSnappingEnabled->setChecked( settings.value( QStringLiteral( "/Processing/Modeler/enableSnapToGrid" ), false ).toBool() );
148  connect( mActionSnappingEnabled, &QAction::toggled, this, [ = ]( bool enabled )
149  {
150  mView->snapper()->setSnapToGrid( enabled );
151  QgsSettings().setValue( QStringLiteral( "/Processing/Modeler/enableSnapToGrid" ), enabled );
152  } );
153  mView->snapper()->setSnapToGrid( mActionSnappingEnabled->isChecked() );
154 
155  connect( mActionSelectAll, &QAction::triggered, this, [ = ]
156  {
157  mScene->selectAll();
158  } );
159 
160  mUndoAction = mUndoStack->createUndoAction( this );
161  mUndoAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionUndo.svg" ) ) );
162  mUndoAction->setShortcuts( QKeySequence::Undo );
163  mRedoAction = mUndoStack->createRedoAction( this );
164  mRedoAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionRedo.svg" ) ) );
165  mRedoAction->setShortcuts( QKeySequence::Redo );
166 
167  mMenuEdit->insertAction( mActionDeleteComponents, mRedoAction );
168  mMenuEdit->insertAction( mActionDeleteComponents, mUndoAction );
169  mMenuEdit->insertSeparator( mActionDeleteComponents );
170  mToolbar->insertAction( mActionZoomIn, mUndoAction );
171  mToolbar->insertAction( mActionZoomIn, mRedoAction );
172  mToolbar->insertSeparator( mActionZoomIn );
173 
174  mGroupMenu = new QMenu( tr( "Zoom To" ), this );
175  mMenuView->insertMenu( mActionZoomIn, mGroupMenu );
176  connect( mGroupMenu, &QMenu::aboutToShow, this, &QgsModelDesignerDialog::populateZoomToMenu );
177 
178  //cut/copy/paste actions. Note these are not included in the ui file
179  //as ui files have no support for QKeySequence shortcuts
180  mActionCut = new QAction( tr( "Cu&t" ), this );
181  mActionCut->setShortcuts( QKeySequence::Cut );
182  mActionCut->setStatusTip( tr( "Cut" ) );
183  mActionCut->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionEditCut.svg" ) ) );
184  connect( mActionCut, &QAction::triggered, this, [ = ]
185  {
186  mView->copySelectedItems( QgsModelGraphicsView::ClipboardCut );
187  } );
188 
189  mActionCopy = new QAction( tr( "&Copy" ), this );
190  mActionCopy->setShortcuts( QKeySequence::Copy );
191  mActionCopy->setStatusTip( tr( "Copy" ) );
192  mActionCopy->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionEditCopy.svg" ) ) );
193  connect( mActionCopy, &QAction::triggered, this, [ = ]
194  {
195  mView->copySelectedItems( QgsModelGraphicsView::ClipboardCopy );
196  } );
197 
198  mActionPaste = new QAction( tr( "&Paste" ), this );
199  mActionPaste->setShortcuts( QKeySequence::Paste );
200  mActionPaste->setStatusTip( tr( "Paste" ) );
201  mActionPaste->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionEditPaste.svg" ) ) );
202  connect( mActionPaste, &QAction::triggered, this, [ = ]
203  {
204  mView->pasteItems( QgsModelGraphicsView::PasteModeCursor );
205  } );
206  mMenuEdit->insertAction( mActionDeleteComponents, mActionCut );
207  mMenuEdit->insertAction( mActionDeleteComponents, mActionCopy );
208  mMenuEdit->insertAction( mActionDeleteComponents, mActionPaste );
209  mMenuEdit->insertSeparator( mActionDeleteComponents );
210 
211  QgsProcessingToolboxProxyModel::Filters filters = QgsProcessingToolboxProxyModel::FilterModeler;
212  if ( settings.value( QStringLiteral( "Processing/Configuration/SHOW_ALGORITHMS_KNOWN_ISSUES" ), false ).toBool() )
213  {
215  }
216  mAlgorithmsTree->setFilters( filters );
217  mAlgorithmsTree->setDragDropMode( QTreeWidget::DragOnly );
218  mAlgorithmsTree->setDropIndicatorShown( true );
219 
220  mAlgorithmsModel = new QgsModelerToolboxModel( this );
221  mAlgorithmsTree->setToolboxProxyModel( mAlgorithmsModel );
222 
223  connect( mView, &QgsModelGraphicsView::algorithmDropped, this, [ = ]( const QString & algorithmId, const QPointF & pos )
224  {
225  addAlgorithm( algorithmId, pos );
226  } );
227  connect( mAlgorithmsTree, &QgsProcessingToolboxTreeView::doubleClicked, this, [ = ]()
228  {
229  if ( mAlgorithmsTree->selectedAlgorithm() )
230  addAlgorithm( mAlgorithmsTree->selectedAlgorithm()->id(), QPointF() );
231  } );
232  connect( mInputsTreeWidget, &QgsModelDesignerInputsTreeWidget::doubleClicked, this, [ = ]( const QModelIndex & )
233  {
234  const QString parameterType = mInputsTreeWidget->currentItem()->data( 0, Qt::UserRole ).toString();
235  addInput( parameterType, QPointF() );
236  } );
237 
238  connect( mView, &QgsModelGraphicsView::inputDropped, this, &QgsModelDesignerDialog::addInput );
239 
240  // Ctrl+= should also trigger a zoom in action
241  QShortcut *ctrlEquals = new QShortcut( QKeySequence( QStringLiteral( "Ctrl+=" ) ), this );
242  connect( ctrlEquals, &QShortcut::activated, this, &QgsModelDesignerDialog::zoomIn );
243 
244  mUndoDock = new QgsDockWidget( tr( "Undo History" ), this );
245  mUndoDock->setObjectName( QStringLiteral( "UndoDock" ) );
246  mUndoView = new QUndoView( mUndoStack, this );
247  mUndoDock->setWidget( mUndoView );
248  mUndoDock->setFeatures( QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetClosable );
249  addDockWidget( Qt::DockWidgetArea::LeftDockWidgetArea, mUndoDock );
250 
251  tabifyDockWidget( mUndoDock, mPropertiesDock );
252  tabifyDockWidget( mVariablesDock, mPropertiesDock );
253  mPropertiesDock->raise();
254  tabifyDockWidget( mInputsDock, mAlgorithmsDock );
255  mInputsDock->raise();
256 
257  connect( mVariablesEditor, &QgsVariableEditorWidget::scopeChanged, this, [ = ]
258  {
259  if ( mModel )
260  {
261  beginUndoCommand( tr( "Change Model Variables" ) );
262  mModel->setVariables( mVariablesEditor->variablesInActiveScope() );
263  endUndoCommand();
264  }
265  } );
266  connect( mNameEdit, &QLineEdit::textChanged, this, [ = ]( const QString & name )
267  {
268  if ( mModel )
269  {
270  beginUndoCommand( tr( "Change Model Name" ), NameChanged );
271  mModel->setName( name );
272  endUndoCommand();
273  updateWindowTitle();
274  }
275  } );
276  connect( mGroupEdit, &QLineEdit::textChanged, this, [ = ]( const QString & group )
277  {
278  if ( mModel )
279  {
280  beginUndoCommand( tr( "Change Model Group" ), GroupChanged );
281  mModel->setGroup( group );
282  endUndoCommand();
283  }
284  } );
285 
286  fillInputsTree();
287 
288  QToolButton *toolbuttonExportToScript = new QToolButton();
289  toolbuttonExportToScript->setPopupMode( QToolButton::InstantPopup );
290  toolbuttonExportToScript->addAction( mActionExportAsScriptAlgorithm );
291  toolbuttonExportToScript->setDefaultAction( mActionExportAsScriptAlgorithm );
292  mToolbar->insertWidget( mActionExportImage, toolbuttonExportToScript );
293  connect( mActionExportAsScriptAlgorithm, &QAction::triggered, this, &QgsModelDesignerDialog::exportAsScriptAlgorithm );
294 
295  mActionShowComments->setChecked( settings.value( QStringLiteral( "/Processing/Modeler/ShowComments" ), true ).toBool() );
296  connect( mActionShowComments, &QAction::toggled, this, &QgsModelDesignerDialog::toggleComments );
297 
298  mSelectTool = new QgsModelViewToolSelect( mView );
299 #if 0
300  mSelectTool->setAction( mActionSelectMoveItem );
301 #endif
302  mView->setTool( mSelectTool );
303  mView->setFocus();
304 
305  connect( mView, &QgsModelGraphicsView::macroCommandStarted, this, [ = ]( const QString & text )
306  {
307  mIgnoreUndoStackChanges++;
308  mUndoStack->beginMacro( text );
309  mIgnoreUndoStackChanges--;
310  } );
311  connect( mView, &QgsModelGraphicsView::macroCommandEnded, this, [ = ]
312  {
313  mIgnoreUndoStackChanges++;
314  mUndoStack->endMacro();
315  mIgnoreUndoStackChanges--;
316  } );
317  connect( mView, &QgsModelGraphicsView::beginCommand, this, [ = ]( const QString & text )
318  {
319  beginUndoCommand( text );
320  } );
321  connect( mView, &QgsModelGraphicsView::endCommand, this, [ = ]
322  {
323  endUndoCommand();
324  } );
325  connect( mView, &QgsModelGraphicsView::deleteSelectedItems, this, [ = ]
326  {
327  deleteSelected();
328  } );
329 
330  connect( mActionAddGroupBox, &QAction::triggered, this, [ = ]
331  {
332  const QPointF viewCenter = mView->mapToScene( mView->viewport()->rect().center() );
333  QgsProcessingModelGroupBox group;
334  group.setPosition( viewCenter );
335  group.setDescription( tr( "New Group" ) );
336 
337  beginUndoCommand( tr( "Add Group Box" ) );
338  model()->addGroupBox( group );
339  repaintModel();
340  endUndoCommand();
341  } );
342  updateWindowTitle();
343 }
344 
345 QgsModelDesignerDialog::~QgsModelDesignerDialog()
346 {
347  mIgnoreUndoStackChanges++;
348  delete mSelectTool; // delete mouse handles before everything else
349 }
350 
351 void QgsModelDesignerDialog::closeEvent( QCloseEvent *event )
352 {
353  if ( checkForUnsavedChanges() )
354  event->accept();
355  else
356  event->ignore();
357 }
358 
359 void QgsModelDesignerDialog::beginUndoCommand( const QString &text, int id )
360 {
361  if ( mBlockUndoCommands || !mUndoStack )
362  return;
363 
364  if ( mActiveCommand )
365  endUndoCommand();
366 
367  mActiveCommand = qgis::make_unique< QgsModelUndoCommand >( mModel.get(), text, id );
368 }
369 
370 void QgsModelDesignerDialog::endUndoCommand()
371 {
372  if ( mBlockUndoCommands || !mActiveCommand || !mUndoStack )
373  return;
374 
375  mActiveCommand->saveAfterState();
376  mIgnoreUndoStackChanges++;
377  mUndoStack->push( mActiveCommand.release() );
378  mIgnoreUndoStackChanges--;
379  setDirty( true );
380 }
381 
382 QgsProcessingModelAlgorithm *QgsModelDesignerDialog::model()
383 {
384  return mModel.get();
385 }
386 
387 void QgsModelDesignerDialog::setModel( QgsProcessingModelAlgorithm *model )
388 {
389  mModel.reset( model );
390 
391  mGroupEdit->setText( mModel->group() );
392  mNameEdit->setText( mModel->displayName() );
393  repaintModel();
394  updateVariablesGui();
395 
396  mView->centerOn( 0, 0 );
397  setDirty( false );
398 
399  mIgnoreUndoStackChanges++;
400  mUndoStack->clear();
401  mIgnoreUndoStackChanges--;
402 
403  updateWindowTitle();
404 }
405 
406 void QgsModelDesignerDialog::loadModel( const QString &path )
407 {
408  std::unique_ptr< QgsProcessingModelAlgorithm > alg = qgis::make_unique< QgsProcessingModelAlgorithm >();
409  if ( alg->fromFile( path ) )
410  {
411  alg->setProvider( QgsApplication::processingRegistry()->providerById( QStringLiteral( "model" ) ) );
412  setModel( alg.release() );
413  }
414  else
415  {
416  QgsMessageLog::logMessage( tr( "Could not load model %1" ).arg( path ), tr( "Processing" ), Qgis::Critical );
417  QMessageBox::critical( this, tr( "Open Model" ), tr( "The selected model could not be loaded.\n"
418  "See the log for more information." ) );
419  }
420 }
421 
422 void QgsModelDesignerDialog::setModelScene( QgsModelGraphicsScene *scene )
423 {
424  QgsModelGraphicsScene *oldScene = mScene;
425 
426  mScene = scene;
427  mScene->setParent( this );
428  mScene->setChildAlgorithmResults( mChildResults );
429  mScene->setModel( mModel.get() );
430  mScene->setMessageBar( mMessageBar );
431 
432  const QPointF center = mView->mapToScene( mView->viewport()->rect().center() );
433  mView->setModelScene( mScene );
434 
435  mSelectTool->resetCache();
436  mSelectTool->setScene( mScene );
437 
438  connect( mScene, &QgsModelGraphicsScene::rebuildRequired, this, [ = ]
439  {
440  if ( mBlockRepaints )
441  return;
442 
443  repaintModel();
444  } );
445  connect( mScene, &QgsModelGraphicsScene::componentAboutToChange, this, [ = ]( const QString & description, int id ) { beginUndoCommand( description, id ); } );
446  connect( mScene, &QgsModelGraphicsScene::componentChanged, this, [ = ] { endUndoCommand(); } );
447 
448  mView->centerOn( center );
449 
450  if ( oldScene )
451  oldScene->deleteLater();
452 }
453 
454 void QgsModelDesignerDialog::updateVariablesGui()
455 {
456  mBlockUndoCommands++;
457 
458  std::unique_ptr< QgsExpressionContextScope > variablesScope = qgis::make_unique< QgsExpressionContextScope >( tr( "Model Variables" ) );
459  const QVariantMap modelVars = mModel->variables();
460  for ( auto it = modelVars.constBegin(); it != modelVars.constEnd(); ++it )
461  {
462  variablesScope->setVariable( it.key(), it.value() );
463  }
464  QgsExpressionContext variablesContext;
465  variablesContext.appendScope( variablesScope.release() );
466  mVariablesEditor->setContext( &variablesContext );
467  mVariablesEditor->setEditableScopeIndex( 0 );
468 
469  mBlockUndoCommands--;
470 }
471 
472 void QgsModelDesignerDialog::setDirty( bool dirty )
473 {
474  mHasChanged = dirty;
475  updateWindowTitle();
476 }
477 
478 bool QgsModelDesignerDialog::validateSave()
479 {
480  if ( mNameEdit->text().trimmed().isEmpty() )
481  {
482  mMessageBar->pushWarning( QString(), tr( "Please a enter model name before saving" ) );
483  return false;
484  }
485 
486  return true;
487 }
488 
489 bool QgsModelDesignerDialog::checkForUnsavedChanges()
490 {
491  if ( isDirty() )
492  {
493  QMessageBox::StandardButton ret = QMessageBox::question( this, tr( "Save Model?" ),
494  tr( "There are unsaved changes in this model. Do you want to keep those?" ),
495  QMessageBox::Save | QMessageBox::Cancel | QMessageBox::Discard, QMessageBox::Cancel );
496  switch ( ret )
497  {
498  case QMessageBox::Save:
499  saveModel( false );
500  return true;
501 
502  case QMessageBox::Discard:
503  return true;
504 
505  default:
506  return false;
507  }
508  }
509  else
510  {
511  return true;
512  }
513 }
514 
515 void QgsModelDesignerDialog::setLastRunChildAlgorithmResults( const QVariantMap &results )
516 {
517  mChildResults = results;
518  if ( mScene )
519  mScene->setChildAlgorithmResults( mChildResults );
520 }
521 
522 void QgsModelDesignerDialog::setLastRunChildAlgorithmInputs( const QVariantMap &inputs )
523 {
524  mChildInputs = inputs;
525  if ( mScene )
526  mScene->setChildAlgorithmInputs( mChildInputs );
527 }
528 
529 void QgsModelDesignerDialog::zoomIn()
530 {
531  mView->setTransformationAnchor( QGraphicsView::NoAnchor );
532  QPointF point = mView->mapToScene( QPoint( mView->viewport()->width() / 2.0, mView->viewport()->height() / 2 ) );
533  QgsSettings settings;
534  const double factor = settings.value( QStringLiteral( "/qgis/zoom_favor" ), 2.0 ).toDouble();
535  mView->scale( factor, factor );
536  mView->centerOn( point );
537 }
538 
539 void QgsModelDesignerDialog::zoomOut()
540 {
541  mView->setTransformationAnchor( QGraphicsView::NoAnchor );
542  QPointF point = mView->mapToScene( QPoint( mView->viewport()->width() / 2.0, mView->viewport()->height() / 2 ) );
543  QgsSettings settings;
544  const double factor = 1.0 / settings.value( QStringLiteral( "/qgis/zoom_favor" ), 2.0 ).toDouble();
545  mView->scale( factor, factor );
546  mView->centerOn( point );
547 }
548 
549 void QgsModelDesignerDialog::zoomActual()
550 {
551  QPointF point = mView->mapToScene( QPoint( mView->viewport()->width() / 2.0, mView->viewport()->height() / 2 ) );
552  mView->resetTransform();
553  mView->scale( QgsApplication::desktop()->logicalDpiX() / 96, QgsApplication::desktop()->logicalDpiX() / 96 );
554  mView->centerOn( point );
555 }
556 
557 void QgsModelDesignerDialog::zoomFull()
558 {
559  QRectF totalRect = mView->scene()->itemsBoundingRect();
560  totalRect.adjust( -10, -10, 10, 10 );
561  mView->fitInView( totalRect, Qt::KeepAspectRatio );
562 }
563 
564 void QgsModelDesignerDialog::exportToImage()
565 {
566  QString filename = QFileDialog::getSaveFileName( this, tr( "Save Model as Image" ), tr( "PNG files (*.png *.PNG)" ) );
567  if ( filename.isEmpty() )
568  return;
569 
570  filename = QgsFileUtils::ensureFileNameHasExtension( filename, QStringList() << QStringLiteral( "png" ) );
571 
572  repaintModel( false );
573 
574  QRectF totalRect = mView->scene()->itemsBoundingRect();
575  totalRect.adjust( -10, -10, 10, 10 );
576  const QRectF imageRect = QRectF( 0, 0, totalRect.width(), totalRect.height() );
577 
578  QImage img( totalRect.width(), totalRect.height(),
579  QImage::Format_ARGB32_Premultiplied );
580  img.fill( Qt::white );
581  QPainter painter;
582  painter.setRenderHint( QPainter::Antialiasing );
583  painter.begin( &img );
584  mView->scene()->render( &painter, imageRect, totalRect );
585  painter.end();
586 
587  img.save( filename );
588 
589  mMessageBar->pushMessage( QString(), tr( "Successfully exported model as image to <a href=\"{}\">{}</a>" ).arg( QUrl::fromLocalFile( filename ).toString(), QDir::toNativeSeparators( filename ) ), Qgis::Success, 5 );
590  repaintModel( true );
591 }
592 
593 void QgsModelDesignerDialog::exportToPdf()
594 {
595  QString filename = QFileDialog::getSaveFileName( this, tr( "Save Model as PDF" ), tr( "PDF files (*.pdf *.PDF)" ) );
596  if ( filename.isEmpty() )
597  return;
598 
599  filename = QgsFileUtils::ensureFileNameHasExtension( filename, QStringList() << QStringLiteral( "pdf" ) );
600 
601  repaintModel( false );
602 
603  QRectF totalRect = mView->scene()->itemsBoundingRect();
604  totalRect.adjust( -10, -10, 10, 10 );
605  const QRectF printerRect = QRectF( 0, 0, totalRect.width(), totalRect.height() );
606 
607  QPrinter printer;
608  printer.setOutputFormat( QPrinter::PdfFormat );
609  printer.setOutputFileName( filename );
610  printer.setPaperSize( QSizeF( printerRect.width(), printerRect.height() ), QPrinter::DevicePixel );
611  printer.setFullPage( true );
612 
613  QPainter painter( &printer );
614  mView->scene()->render( &painter, printerRect, totalRect );
615  painter.end();
616 
617  mMessageBar->pushMessage( QString(), tr( "Successfully exported model as PDF to <a href=\"{}\">{}</a>" ).arg( QUrl::fromLocalFile( filename ).toString(), QDir::toNativeSeparators( filename ) ), Qgis::Success, 5 );
618  repaintModel( true );
619 }
620 
621 void QgsModelDesignerDialog::exportToSvg()
622 {
623  QString filename = QFileDialog::getSaveFileName( this, tr( "Save Model as SVG" ), tr( "SVG files (*.svg *.SVG)" ) );
624  if ( filename.isEmpty() )
625  return;
626 
627  filename = QgsFileUtils::ensureFileNameHasExtension( filename, QStringList() << QStringLiteral( "svg" ) );
628 
629  repaintModel( false );
630 
631  QRectF totalRect = mView->scene()->itemsBoundingRect();
632  totalRect.adjust( -10, -10, 10, 10 );
633  const QRectF svgRect = QRectF( 0, 0, totalRect.width(), totalRect.height() );
634 
635  QSvgGenerator svg;
636  svg.setFileName( filename );
637  svg.setSize( QSize( totalRect.width(), totalRect.height() ) );
638  svg.setViewBox( svgRect );
639  svg.setTitle( mModel->displayName() );
640 
641  QPainter painter( &svg );
642  mView->scene()->render( &painter, svgRect, totalRect );
643  painter.end();
644 
645  mMessageBar->pushMessage( QString(), tr( "Successfully exported model as SVG to <a href=\"{}\">{}</a>" ).arg( QUrl::fromLocalFile( filename ).toString(), QDir::toNativeSeparators( filename ) ), Qgis::Success, 5 );
646  repaintModel( true );
647 }
648 
649 void QgsModelDesignerDialog::exportAsPython()
650 {
651  QString filename = QFileDialog::getSaveFileName( this, tr( "Save Model as Python Script" ), tr( "Processing scripts (*.py *.PY)" ) );
652  if ( filename.isEmpty() )
653  return;
654 
655  filename = QgsFileUtils::ensureFileNameHasExtension( filename, QStringList() << QStringLiteral( "py" ) );
656 
657  const QString text = mModel->asPythonCode( QgsProcessing::PythonQgsProcessingAlgorithmSubclass, 4 ).join( '\n' );
658 
659  QFile outFile( filename );
660  if ( !outFile.open( QIODevice::WriteOnly | QIODevice::Truncate ) )
661  {
662  return;
663  }
664  QTextStream fout( &outFile );
665  fout << text;
666  outFile.close();
667 
668  mMessageBar->pushMessage( QString(), tr( "Successfully exported model as Python script to <a href=\"{}\">{}</a>" ).arg( QUrl::fromLocalFile( filename ).toString(), QDir::toNativeSeparators( filename ) ), Qgis::Success, 5 );
669 }
670 
671 void QgsModelDesignerDialog::toggleComments( bool show )
672 {
673  QgsSettings().setValue( QStringLiteral( "/Processing/Modeler/ShowComments" ), show );
674 
675  repaintModel( true );
676 }
677 
678 void QgsModelDesignerDialog::updateWindowTitle()
679 {
680  QString title = tr( "Model Designer" );
681  if ( !mModel->name().isEmpty() )
682  title = QStringLiteral( "%1 - %2" ).arg( title, mModel->name() );
683 
684  if ( isDirty() )
685  title.prepend( '*' );
686 
687  setWindowTitle( title );
688 }
689 
690 void QgsModelDesignerDialog::deleteSelected()
691 {
692  QList< QgsModelComponentGraphicItem * > items = mScene->selectedComponentItems();
693  if ( items.empty() )
694  return;
695 
696  if ( items.size() == 1 )
697  {
698  items.at( 0 )->deleteComponent();
699  return;
700  }
701 
702  std::sort( items.begin(), items.end(), []( QgsModelComponentGraphicItem * p1, QgsModelComponentGraphicItem * p2 )
703  {
704  // try to delete the easy stuff first, so comments, then outputs, as nothing will depend on these...
705  if ( dynamic_cast< QgsModelCommentGraphicItem *>( p1 ) )
706  return true;
707  else if ( dynamic_cast< QgsModelCommentGraphicItem *>( p2 ) )
708  return false;
709  else if ( dynamic_cast< QgsModelGroupBoxGraphicItem *>( p1 ) )
710  return true;
711  else if ( dynamic_cast< QgsModelGroupBoxGraphicItem *>( p2 ) )
712  return false;
713  else if ( dynamic_cast< QgsModelOutputGraphicItem *>( p1 ) )
714  return true;
715  else if ( dynamic_cast< QgsModelOutputGraphicItem *>( p2 ) )
716  return false;
717  else if ( dynamic_cast< QgsModelChildAlgorithmGraphicItem *>( p1 ) )
718  return true;
719  else if ( dynamic_cast< QgsModelChildAlgorithmGraphicItem *>( p2 ) )
720  return false;
721  return false;
722  } );
723 
724 
725  beginUndoCommand( tr( "Delete Components" ) );
726 
727  QVariant prevState = mModel->toVariant();
728  mBlockUndoCommands++;
729  mBlockRepaints = true;
730  bool failed = false;
731  while ( !items.empty() )
732  {
733  QgsModelComponentGraphicItem *toDelete = nullptr;
734  for ( QgsModelComponentGraphicItem *item : items )
735  {
736  if ( item->canDeleteComponent() )
737  {
738  toDelete = item;
739  break;
740  }
741  }
742 
743  if ( !toDelete )
744  {
745  failed = true;
746  break;
747  }
748 
749  toDelete->deleteComponent();
750  items.removeAll( toDelete );
751  }
752 
753  if ( failed )
754  {
755  mModel->loadVariant( prevState );
756  QMessageBox::warning( nullptr, QObject::tr( "Could not remove components" ),
757  QObject::tr( "Components depend on the selected items.\n"
758  "Try to remove them before trying deleting these components." ) );
759  mBlockUndoCommands--;
760  mActiveCommand.reset();
761  }
762  else
763  {
764  mBlockUndoCommands--;
765  endUndoCommand();
766  }
767 
768  mBlockRepaints = false;
769  repaintModel();
770 }
771 
772 void QgsModelDesignerDialog::populateZoomToMenu()
773 {
774  mGroupMenu->clear();
775  for ( const QgsProcessingModelGroupBox &box : model()->groupBoxes() )
776  {
777  if ( QgsModelComponentGraphicItem *item = mScene->groupBoxItem( box.uuid() ) )
778  {
779  QAction *zoomAction = new QAction( box.description(), mGroupMenu );
780  connect( zoomAction, &QAction::triggered, this, [ = ]
781  {
782  mView->centerOn( item );
783  } );
784  mGroupMenu->addAction( zoomAction );
785  }
786  }
787 }
788 
789 void QgsModelDesignerDialog::validate()
790 {
791  QStringList issues;
792  if ( model()->validate( issues ) )
793  {
794  mMessageBar->pushSuccess( QString(), tr( "Model is valid!" ) );
795  }
796  else
797  {
798  QgsMessageBarItem *messageWidget = mMessageBar->createMessage( QString(), tr( "Model is invalid!" ) );
799  QPushButton *detailsButton = new QPushButton( tr( "Details" ) );
800  connect( detailsButton, &QPushButton::clicked, detailsButton, [ = ]
801  {
802  QgsMessageViewer *dialog = new QgsMessageViewer( detailsButton );
803  dialog->setTitle( tr( "Model is Invalid" ) );
804 
805  QString longMessage = tr( "<p>This model is not valid:</p>" ) + QStringLiteral( "<ul>" );
806  for ( const QString &issue : issues )
807  {
808  longMessage += QStringLiteral( "<li>%1</li>" ).arg( issue );
809  }
810  longMessage += QLatin1String( "</ul>" );
811 
812  dialog->setMessage( longMessage, QgsMessageOutput::MessageHtml );
813  dialog->showMessage();
814  } );
815  messageWidget->layout()->addWidget( detailsButton );
816  mMessageBar->clearWidgets();
817  mMessageBar->pushWidget( messageWidget, Qgis::Warning, 0 );
818  }
819 }
820 
821 void QgsModelDesignerDialog::reorderInputs()
822 {
823  QgsModelInputReorderDialog dlg( this );
824  dlg.setModel( mModel.get() );
825  if ( dlg.exec() )
826  {
827  const QStringList inputOrder = dlg.inputOrder();
828  beginUndoCommand( tr( "Reorder Inputs" ) );
829  mModel->setParameterOrder( inputOrder );
830  endUndoCommand();
831  }
832 }
833 
834 bool QgsModelDesignerDialog::isDirty() const
835 {
836  return mHasChanged && mUndoStack->index() != -1;
837 }
838 
839 void QgsModelDesignerDialog::fillInputsTree()
840 {
841  const QIcon icon = QgsApplication::getThemeIcon( QStringLiteral( "mIconModelInput.svg" ) );
842  std::unique_ptr< QTreeWidgetItem > parametersItem = qgis::make_unique< QTreeWidgetItem >();
843  parametersItem->setText( 0, tr( "Parameters" ) );
844  QList<QgsProcessingParameterType *> available = QgsApplication::processingRegistry()->parameterTypes();
845  std::sort( available.begin(), available.end(), []( const QgsProcessingParameterType * a, const QgsProcessingParameterType * b ) -> bool
846  {
847  return QString::localeAwareCompare( a->name(), b->name() ) < 0;
848  } );
849 
850  for ( QgsProcessingParameterType *param : qgis::as_const( available ) )
851  {
852  if ( param->flags() & QgsProcessingParameterType::ExposeToModeler )
853  {
854  std::unique_ptr< QTreeWidgetItem > paramItem = qgis::make_unique< QTreeWidgetItem >();
855  paramItem->setText( 0, param->name() );
856  paramItem->setData( 0, Qt::UserRole, param->id() );
857  paramItem->setIcon( 0, icon );
858  paramItem->setFlags( Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled );
859  paramItem->setToolTip( 0, param->description() );
860  parametersItem->addChild( paramItem.release() );
861  }
862  }
863  mInputsTreeWidget->addTopLevelItem( parametersItem.release() );
864  mInputsTreeWidget->topLevelItem( 0 )->setExpanded( true );
865 }
866 
867 
868 //
869 // QgsModelChildDependenciesWidget
870 //
871 
872 QgsModelChildDependenciesWidget::QgsModelChildDependenciesWidget( QWidget *parent, QgsProcessingModelAlgorithm *model, const QString &childId )
873  : QWidget( parent )
874  , mModel( model )
875  , mChildId( childId )
876 {
877  QHBoxLayout *hl = new QHBoxLayout();
878  hl->setContentsMargins( 0, 0, 0, 0 );
879 
880  mLineEdit = new QLineEdit();
881  mLineEdit->setEnabled( false );
882  hl->addWidget( mLineEdit, 1 );
883 
884  mToolButton = new QToolButton();
885  mToolButton->setText( QString( QChar( 0x2026 ) ) );
886  hl->addWidget( mToolButton );
887 
888  setLayout( hl );
889 
890  mLineEdit->setText( tr( "%1 dependencies selected" ).arg( 0 ) );
891 
892  connect( mToolButton, &QToolButton::clicked, this, &QgsModelChildDependenciesWidget::showDialog );
893 }
894 
895 void QgsModelChildDependenciesWidget::setValue( const QList<QgsProcessingModelChildDependency> &value )
896 {
897  mValue = value;
898 
899  updateSummaryText();
900 }
901 
902 void QgsModelChildDependenciesWidget::showDialog()
903 {
904  const QList<QgsProcessingModelChildDependency> available = mModel->availableDependenciesForChildAlgorithm( mChildId );
905 
906  QVariantList availableOptions;
907  for ( const QgsProcessingModelChildDependency &dep : available )
908  availableOptions << QVariant::fromValue( dep );
909  QVariantList selectedOptions;
910  for ( const QgsProcessingModelChildDependency &dep : mValue )
911  selectedOptions << QVariant::fromValue( dep );
912 
914  if ( panel )
915  {
916  QgsProcessingMultipleSelectionPanelWidget *widget = new QgsProcessingMultipleSelectionPanelWidget( availableOptions, selectedOptions );
917  widget->setPanelTitle( tr( "Algorithm Dependencies" ) );
918 
919  widget->setValueFormatter( [ = ]( const QVariant & v ) -> QString
920  {
921  const QgsProcessingModelChildDependency dep = v.value< QgsProcessingModelChildDependency >();
922 
923  const QString description = mModel->childAlgorithm( dep.childId ).description();
924  if ( dep.conditionalBranch.isEmpty() )
925  return description;
926  else
927  return tr( "Condition “%1” from algorithm “%2”" ).arg( dep.conditionalBranch, description );
928  } );
929 
930  connect( widget, &QgsProcessingMultipleSelectionPanelWidget::selectionChanged, this, [ = ]()
931  {
932  QList< QgsProcessingModelChildDependency > res;
933  for ( const QVariant &v : widget->selectedOptions() )
934  {
935  res << v.value< QgsProcessingModelChildDependency >();
936  }
937  setValue( res );
938  } );
939  connect( widget, &QgsProcessingMultipleSelectionPanelWidget::acceptClicked, widget, &QgsPanelWidget::acceptPanel );
940  panel->openPanel( widget );
941  }
942 }
943 
944 void QgsModelChildDependenciesWidget::updateSummaryText()
945 {
946  mLineEdit->setText( tr( "%1 dependencies selected" ).arg( mValue.count() ) );
947 }
948 
QgsExpressionContext
Expression contexts are used to encapsulate the parameters around which a QgsExpression should be eva...
Definition: qgsexpressioncontext.h:370
QgsApplication::processingRegistry
static QgsProcessingRegistry * processingRegistry()
Returns the application's processing registry, used for managing processing providers,...
Definition: qgsapplication.cpp:2253
qgsprocessingparametertype.h
QgsModelViewToolSelect
Model designer view tool for selecting items in the model.
Definition: qgsmodelviewtoolselect.h:37
qgsmodelviewtoolselect.h
QgsApplication::getThemeIcon
static QIcon getThemeIcon(const QString &name)
Helper to get a theme icon.
Definition: qgsapplication.cpp:626
qgsmessagebaritem.h
QgsSettings::value
QVariant value(const QString &key, const QVariant &defaultValue=QVariant(), Section section=NoSection) const
Returns the value for setting key.
Definition: qgssettings.cpp:174
QgsPanelWidget::findParentPanel
static QgsPanelWidget * findParentPanel(QWidget *widget)
Traces through the parents of a widget to find if it is contained within a QgsPanelWidget widget.
Definition: qgspanelwidget.cpp:49
qgsgui.h
Qgis::Warning
@ Warning
Definition: qgis.h:91
QgsPanelWidget::openPanel
void openPanel(QgsPanelWidget *panel)
Open a panel or dialog depending on dock mode setting If dock mode is true this method will emit the ...
Definition: qgspanelwidget.cpp:79
QgsProcessingRegistry::parameterTypes
QList< QgsProcessingParameterType * > parameterTypes() const
Returns a list with all known parameter types.
Definition: qgsprocessingregistry.cpp:225
Qgis::Success
@ Success
Definition: qgis.h:93
QgsSettings
This class is a composition of two QSettings instances:
Definition: qgssettings.h:62
QgsProcessingParameterType::ExposeToModeler
@ ExposeToModeler
Is this parameter available in the modeler. Is set to on by default.
Definition: qgsprocessingparametertype.h:43
QgsVariableEditorWidget::scopeChanged
void scopeChanged()
Emitted when the user has modified a scope using the widget.
QgsProcessingParameterType
Makes metadata of processing parameters available.
Definition: qgsprocessingparametertype.h:34
qgsapplication.h
QgsGui::enableAutoGeometryRestore
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:139
qgsmodelinputreorderwidget.h
QgsProcessing::PythonQgsProcessingAlgorithmSubclass
@ PythonQgsProcessingAlgorithmSubclass
Full Python QgsProcessingAlgorithm subclass.
Definition: qgsprocessing.h:60
QgsPanelWidget
Base class for any widget that can be shown as a inline panel.
Definition: qgspanelwidget.h:30
QgsProcessingToolboxProxyModel
A sort/filter proxy model for providers and algorithms shown within the Processing toolbox,...
Definition: qgsprocessingtoolboxmodel.h:418
QgsProcessingToolboxProxyModel::FilterModeler
@ FilterModeler
Filters out any algorithms and content which should not be shown in the modeler.
Definition: qgsprocessingtoolboxmodel.h:427
QgsMessageBarItem
Represents an item shown within a QgsMessageBar widget.
Definition: qgsmessagebaritem.h:39
qgsprocessingalgorithm.h
QgsPanelWidget::acceptPanel
void acceptPanel()
Accept the panel.
Definition: qgspanelwidget.cpp:107
qgsmessageviewer.h
qgsmodelundocommand.h
QgsMessageBar
A bar for displaying non-blocking messages to the user.
Definition: qgsmessagebar.h:61
qgsmodeldesignerdialog.h
qgsmessagebar.h
QgsSettings::setValue
void setValue(const QString &key, const QVariant &value, QgsSettings::Section section=QgsSettings::NoSection)
Sets the value of setting key to value.
Definition: qgssettings.cpp:289
QgsProcessingToolboxProxyModel::FilterShowKnownIssues
@ FilterShowKnownIssues
Show algorithms with known issues (hidden by default)
Definition: qgsprocessingtoolboxmodel.h:429
qgsmodelcomponentgraphicitem.h
qgsfileutils.h
qgsmodelgraphicsscene.h
QgsExpressionContext::appendScope
void appendScope(QgsExpressionContextScope *scope)
Appends a scope to the end of the context.
Definition: qgsexpressioncontext.cpp:490
QgsFileUtils::ensureFileNameHasExtension
static QString ensureFileNameHasExtension(const QString &fileName, const QStringList &extensions)
Ensures that a fileName ends with an extension from the provided list of extensions.
Definition: qgsfileutils.cpp:59
qgssettings.h
QgsMessageLog::logMessage
static void logMessage(const QString &message, const QString &tag=QString(), Qgis::MessageLevel level=Qgis::Warning, bool notifyUser=true)
Adds a message to the log instance (and creates it if necessary).
Definition: qgsmessagelog.cpp:27
QgsDockWidget
QgsDockWidget subclass with more fine-grained control over how the widget is closed or opened.
Definition: qgsdockwidget.h:32
QgsMessageOutput::MessageHtml
@ MessageHtml
Definition: qgsmessageoutput.h:47
Qgis::Critical
@ Critical
Definition: qgis.h:92
qgsprocessingmultipleselectiondialog.h
qgspanelwidget.h
qgsprocessingregistry.h