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