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