diff --git a/python/gui/auto_generated/processing/models/qgsmodeldesignerdialog.sip.in b/python/gui/auto_generated/processing/models/qgsmodeldesignerdialog.sip.in
index afdd9748476..8358ac26899 100644
--- a/python/gui/auto_generated/processing/models/qgsmodeldesignerdialog.sip.in
+++ b/python/gui/auto_generated/processing/models/qgsmodeldesignerdialog.sip.in
@@ -9,6 +9,7 @@
+
class QgsModelDesignerDialog : QMainWindow
{
%Docstring
@@ -26,11 +27,26 @@ Model designer dialog base class
%End
public:
- QgsModelDesignerDialog( QWidget *parent = 0, Qt::WindowFlags flags = 0 );
+ QgsModelDesignerDialog( QWidget *parent /TransferThis/ = 0, Qt::WindowFlags flags = 0 );
protected:
+ virtual void repaintModel( bool showControls = true ) = 0;
+ virtual QgsProcessingModelAlgorithm *model() = 0;
+ virtual void addAlgorithm( const QString &algorithmId, const QPointF &pos ) = 0;
+ virtual void addInput( const QString &inputId, const QPointF &pos ) = 0;
+
QToolBar *toolbar();
+ QAction *actionOpen();
+ QAction *actionSave();
+ QAction *actionSaveAs();
+ QAction *actionSaveInProject();
+ QAction *actionEditHelp();
+ QAction *actionRun();
+ QAction *actionExportImage();
+
+ QgsMessageBar *messageBar();
+ QGraphicsView *view();
};
diff --git a/python/gui/auto_generated/processing/models/qgsmodelgraphicsview.sip.in b/python/gui/auto_generated/processing/models/qgsmodelgraphicsview.sip.in
new file mode 100644
index 00000000000..278a25d1936
--- /dev/null
+++ b/python/gui/auto_generated/processing/models/qgsmodelgraphicsview.sip.in
@@ -0,0 +1,70 @@
+/************************************************************************
+ * This file has been generated automatically from *
+ * *
+ * src/gui/processing/models/qgsmodelgraphicsview.h *
+ * *
+ * Do not edit manually ! Edit header and run scripts/sipify.pl again *
+ ************************************************************************/
+
+
+
+
+class QgsModelGraphicsView : QGraphicsView
+{
+%Docstring
+QGraphicsView subclass representing the model designer.
+
+.. warning::
+
+ Not stable API
+
+.. versionadded:: 3.14
+%End
+
+%TypeHeaderCode
+#include "qgsmodelgraphicsview.h"
+%End
+ public:
+
+ QgsModelGraphicsView( QWidget *parent = 0 );
+%Docstring
+Constructor for QgsModelGraphicsView, with the specified ``parent`` widget.
+%End
+
+ virtual void dragEnterEvent( QDragEnterEvent *event );
+
+ virtual void dropEvent( QDropEvent *event );
+
+ virtual void dragMoveEvent( QDragMoveEvent *event );
+
+ virtual void wheelEvent( QWheelEvent *event );
+
+ virtual void enterEvent( QEvent *event );
+
+ virtual void mousePressEvent( QMouseEvent *event );
+
+ virtual void mouseMoveEvent( QMouseEvent *event );
+
+
+ signals:
+
+ void algorithmDropped( const QString &algorithmId, const QPointF &pos );
+%Docstring
+Emitted when an algorithm is dropped onto the view.
+%End
+
+ void inputDropped( const QString &inputId, const QPointF &pos );
+%Docstring
+Emitted when an input parameter is dropped onto the view.
+%End
+
+};
+
+
+/************************************************************************
+ * This file has been generated automatically from *
+ * *
+ * src/gui/processing/models/qgsmodelgraphicsview.h *
+ * *
+ * Do not edit manually ! Edit header and run scripts/sipify.pl again *
+ ************************************************************************/
diff --git a/python/gui/gui_auto.sip b/python/gui/gui_auto.sip
index 2f0216449df..0907a2e7950 100644
--- a/python/gui/gui_auto.sip
+++ b/python/gui/gui_auto.sip
@@ -314,6 +314,7 @@
%Include auto_generated/processing/models/qgsmodeldesignerdialog.sip
%Include auto_generated/processing/models/qgsmodelgraphicitem.sip
%Include auto_generated/processing/models/qgsmodelgraphicsscene.sip
+%Include auto_generated/processing/models/qgsmodelgraphicsview.sip
%Include auto_generated/raster/qgscolorrampshaderwidget.sip
%Include auto_generated/raster/qgshillshaderendererwidget.sip
%Include auto_generated/raster/qgsmultibandcolorrendererwidget.sip
diff --git a/python/plugins/processing/ProcessingPlugin.py b/python/plugins/processing/ProcessingPlugin.py
index b7cf9c76187..51d7484a543 100644
--- a/python/plugins/processing/ProcessingPlugin.py
+++ b/python/plugins/processing/ProcessingPlugin.py
@@ -122,7 +122,7 @@ class ProcessingModelItem(QgsDataItem):
ProcessingDropHandler.runAlg(self.path())
def editModel(self):
- dlg = ModelerDialog()
+ dlg = ModelerDialog.create()
dlg.loadModel(self.path())
dlg.show()
@@ -332,7 +332,7 @@ class ProcessingPlugin:
self.toolboxAction.setChecked(visible)
def openModeler(self):
- dlg = ModelerDialog()
+ dlg = ModelerDialog.create()
dlg.update_model.connect(self.updateModel)
dlg.show()
diff --git a/python/plugins/processing/modeler/CreateNewModelAction.py b/python/plugins/processing/modeler/CreateNewModelAction.py
index f673fdc1169..4ed9c63a3c4 100644
--- a/python/plugins/processing/modeler/CreateNewModelAction.py
+++ b/python/plugins/processing/modeler/CreateNewModelAction.py
@@ -26,6 +26,7 @@ import os
from qgis.PyQt.QtCore import QCoreApplication
from qgis.core import QgsApplication
+from qgis.utils import iface
from processing.gui.ToolboxAction import ToolboxAction
from processing.modeler.ModelerDialog import ModelerDialog
@@ -43,7 +44,7 @@ class CreateNewModelAction(ToolboxAction):
return QgsApplication.getThemeIcon("/processingModel.svg")
def execute(self):
- dlg = ModelerDialog()
+ dlg = ModelerDialog.create()
dlg.update_model.connect(self.updateModel)
dlg.show()
diff --git a/python/plugins/processing/modeler/EditModelAction.py b/python/plugins/processing/modeler/EditModelAction.py
index 74addfbb353..38ba88a5d93 100644
--- a/python/plugins/processing/modeler/EditModelAction.py
+++ b/python/plugins/processing/modeler/EditModelAction.py
@@ -44,7 +44,7 @@ class EditModelAction(ContextAction):
if not ok:
iface.messageBar().pushMessage(QCoreApplication.translate('EditModelAction', 'Cannot edit model: {}').format(msg), level=Qgis.Warning)
else:
- dlg = ModelerDialog(alg)
+ dlg = ModelerDialog.create(alg)
dlg.update_model.connect(self.updateModel)
dlg.show()
diff --git a/python/plugins/processing/modeler/ModelerDialog.py b/python/plugins/processing/modeler/ModelerDialog.py
index 7e2d5eddeb2..4dd4faea3c4 100644
--- a/python/plugins/processing/modeler/ModelerDialog.py
+++ b/python/plugins/processing/modeler/ModelerDialog.py
@@ -21,12 +21,9 @@ __author__ = 'Victor Olaya'
__date__ = 'August 2012'
__copyright__ = '(C) 2012, Victor Olaya'
-import codecs
import sys
import os
-import warnings
-from qgis.PyQt import uic
from qgis.PyQt.QtCore import (
Qt,
QCoreApplication,
@@ -35,22 +32,14 @@ from qgis.PyQt.QtCore import (
QMimeData,
QPoint,
QPointF,
- QByteArray,
- QSize,
- QSizeF,
pyqtSignal,
- QDataStream,
- QIODevice,
- QUrl,
- QTimer)
-from qgis.PyQt.QtWidgets import (QGraphicsView,
- QTreeWidget,
+ QUrl)
+from qgis.PyQt.QtWidgets import (QTreeWidget,
QMessageBox,
QFileDialog,
QTreeWidgetItem,
QSizePolicy,
QMainWindow,
- QShortcut,
QLabel,
QDockWidget,
QWidget,
@@ -60,12 +49,7 @@ from qgis.PyQt.QtWidgets import (QGraphicsView,
QLineEdit,
QToolButton,
QAction)
-from qgis.PyQt.QtGui import (QIcon,
- QImage,
- QPainter,
- QKeySequence)
-from qgis.PyQt.QtSvg import QSvgGenerator
-from qgis.PyQt.QtPrintSupport import QPrinter
+from qgis.PyQt.QtGui import QIcon
from qgis.core import (Qgis,
QgsApplication,
QgsProcessing,
@@ -78,8 +62,7 @@ from qgis.core import (Qgis,
QgsExpressionContextScope,
QgsExpressionContext
)
-from qgis.gui import (QgsMessageBar,
- QgsDockWidget,
+from qgis.gui import (QgsDockWidget,
QgsScrollArea,
QgsFilterLineEdit,
QgsProcessingToolboxTreeView,
@@ -101,7 +84,6 @@ from processing.core.ProcessingConfig import ProcessingConfig
from processing.tools.dataobjects import createContext
from qgis.utils import iface
-
pluginPath = os.path.split(os.path.dirname(__file__))[0]
@@ -134,11 +116,24 @@ class ModelerDialog(QgsModelDesignerDialog):
update_model = pyqtSignal()
- def __init__(self, model=None):
- super().__init__(None)
- self.setAttribute(Qt.WA_DeleteOnClose)
+ dlgs = []
+ @staticmethod
+ def create(model=None):
+ """
+ Workaround crappy sip handling of QMainWindow. It doesn't know that we are using the deleteonclose
+ flag, so happily just deletes dialogs as soon as they go out of scope. The only workaround possible
+ while we still have to drag around this Python code is to store a reference to the sip wrapper so that
+ sip doesn't get confused. The underlying object will still be deleted by the deleteonclose flag though!
+ """
+ dlg = ModelerDialog(model)
+ ModelerDialog.dlgs.append(dlg)
+ return dlg
+
+ def __init__(self, model=None, parent=None):
+ super().__init__(parent)
self._variables_scope = None
+ self._model = None
# LOTS of bug reports when we include the dock creation in the UI file
# see e.g. #16428, #19068
@@ -258,10 +253,6 @@ class ModelerDialog(QgsModelDesignerDialog):
self.tabifyDockWidget(self.propertiesDock, self.variables_dock)
self.variables_editor.scopeChanged.connect(self.variables_changed)
- self.bar = QgsMessageBar()
- self.bar.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
- self.centralWidget().layout().insertWidget(0, self.bar)
-
try:
self.setDockOptions(self.dockOptions() | QMainWindow.GroupedDragging)
except:
@@ -273,10 +264,11 @@ class ModelerDialog(QgsModelDesignerDialog):
self.toolbutton_export_to_script = QToolButton()
self.toolbutton_export_to_script.setPopupMode(QToolButton.InstantPopup)
- self.export_to_script_algorithm_action = QAction(QCoreApplication.translate('ModelerDialog', 'Export as Script Algorithm…'))
+ self.export_to_script_algorithm_action = QAction(
+ QCoreApplication.translate('ModelerDialog', 'Export as Script Algorithm…'))
self.toolbutton_export_to_script.addActions([self.export_to_script_algorithm_action])
self.toolbutton_export_to_script.setDefaultAction(self.export_to_script_algorithm_action)
- self.toolbar().insertWidget(self.mActionExportImage, self.toolbutton_export_to_script)
+ self.toolbar().insertWidget(self.actionExportImage(), self.toolbutton_export_to_script)
self.export_to_script_algorithm_action.triggered.connect(self.export_as_script_algorithm)
self.addDockWidget(Qt.LeftDockWidgetArea, self.propertiesDock)
@@ -285,111 +277,14 @@ class ModelerDialog(QgsModelDesignerDialog):
self.tabifyDockWidget(self.inputsDock, self.algorithmsDock)
self.inputsDock.raise_()
- self.setWindowFlags(Qt.WindowMinimizeButtonHint |
- Qt.WindowMaximizeButtonHint |
- Qt.WindowCloseButtonHint)
-
- settings = QgsSettings()
- self.restoreState(settings.value("/Processing/stateModeler", QByteArray()))
- self.restoreGeometry(settings.value("/Processing/geometryModeler", QByteArray()))
-
self.scene = ModelerScene(self)
self.scene.setSceneRect(QRectF(0, 0, self.CANVAS_SIZE, self.CANVAS_SIZE))
self.scene.rebuildRequired.connect(self.repaintModel)
self.scene.componentChanged.connect(self.componentChanged)
- self.view.setScene(self.scene)
- self.view.setAcceptDrops(True)
- self.view.ensureVisible(0, 0, 10, 10)
- self.view.scale(QgsApplication.desktop().logicalDpiX() / 96, QgsApplication.desktop().logicalDpiX() / 96)
-
- def _dragEnterEvent(event):
- if event.mimeData().hasText() or event.mimeData().hasFormat('application/x-vnd.qgis.qgis.algorithmid'):
- event.acceptProposedAction()
- else:
- event.ignore()
-
- def _dropEvent(event):
- def alg_dropped(algorithm_id, pos):
- alg = QgsApplication.processingRegistry().createAlgorithmById(algorithm_id)
- if alg is not None:
- self._addAlgorithm(alg, pos)
- else:
- assert False, algorithm_id
-
- def input_dropped(id, pos):
- if id in [param.id() for param in QgsApplication.instance().processingRegistry().parameterTypes()]:
- self.addInputOfType(itemId, pos)
-
- if event.mimeData().hasFormat('application/x-vnd.qgis.qgis.algorithmid'):
- data = event.mimeData().data('application/x-vnd.qgis.qgis.algorithmid')
- stream = QDataStream(data, QIODevice.ReadOnly)
- algorithm_id = stream.readQString()
- QTimer.singleShot(0, lambda id=algorithm_id, pos=self.view.mapToScene(event.pos()): alg_dropped(id, pos))
- event.accept()
- elif event.mimeData().hasText():
- itemId = event.mimeData().text()
- QTimer.singleShot(0, lambda id=itemId, pos=self.view.mapToScene(event.pos()): input_dropped(id, pos))
- event.accept()
- else:
- event.ignore()
-
- def _dragMoveEvent(event):
- if event.mimeData().hasText() or event.mimeData().hasFormat('application/x-vnd.qgis.qgis.algorithmid'):
- event.accept()
- else:
- event.ignore()
-
- def _wheelEvent(event):
- self.view.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
-
- settings = QgsSettings()
- factor = settings.value('/qgis/zoom_favor', 2.0)
-
- # "Normal" mouse has an angle delta of 120, precision mouses provide data
- # faster, in smaller steps
- factor = 1.0 + (factor - 1.0) / 120.0 * abs(event.angleDelta().y())
-
- if (event.modifiers() == Qt.ControlModifier):
- factor = 1.0 + (factor - 1.0) / 20.0
-
- if event.angleDelta().y() < 0:
- factor = 1 / factor
-
- self.view.scale(factor, factor)
-
- def _enterEvent(e):
- QGraphicsView.enterEvent(self.view, e)
- self.view.viewport().setCursor(Qt.ArrowCursor)
-
- def _mouseReleaseEvent(e):
- QGraphicsView.mouseReleaseEvent(self.view, e)
- self.view.viewport().setCursor(Qt.ArrowCursor)
-
- def _mousePressEvent(e):
- if e.button() == Qt.MidButton:
- self.previousMousePos = e.pos()
- else:
- QGraphicsView.mousePressEvent(self.view, e)
-
- def _mouseMoveEvent(e):
- if e.buttons() == Qt.MidButton:
- offset = self.previousMousePos - e.pos()
- self.previousMousePos = e.pos()
-
- self.view.verticalScrollBar().setValue(self.view.verticalScrollBar().value() + offset.y())
- self.view.horizontalScrollBar().setValue(self.view.horizontalScrollBar().value() + offset.x())
- else:
- QGraphicsView.mouseMoveEvent(self.view, e)
-
- self.view.setDragMode(QGraphicsView.ScrollHandDrag)
- self.view.dragEnterEvent = _dragEnterEvent
- self.view.dropEvent = _dropEvent
- self.view.dragMoveEvent = _dragMoveEvent
- self.view.wheelEvent = _wheelEvent
- self.view.enterEvent = _enterEvent
- self.view.mousePressEvent = _mousePressEvent
- self.view.mouseMoveEvent = _mouseMoveEvent
+ self.view().setScene(self.scene)
+ self.view().ensureVisible(0, 0, 10, 10)
+ self.view().scale(QgsApplication.desktop().logicalDpiX() / 96, QgsApplication.desktop().logicalDpiX() / 96)
def _mimeDataInput(items):
mimeData = QMimeData()
@@ -420,54 +315,40 @@ class ModelerDialog(QgsModelDesignerDialog):
self.textGroup.setPlaceholderText(self.tr('Enter group name here'))
# Connect signals and slots
- self.inputsTree.doubleClicked.connect(self.addInput)
+ self.inputsTree.doubleClicked.connect(self._addInput)
self.searchBox.textChanged.connect(self.algorithmTree.setFilterString)
- self.algorithmTree.doubleClicked.connect(self.addAlgorithm)
+ self.algorithmTree.doubleClicked.connect(self._addAlgorithm)
- # Ctrl+= should also trigger a zoom in action
- ctrlEquals = QShortcut(QKeySequence("Ctrl+="), self)
- ctrlEquals.activated.connect(self.zoomIn)
+ self.actionOpen().triggered.connect(self.openModel)
+ self.actionSave().triggered.connect(self.save)
+ self.actionSaveAs().triggered.connect(self.saveAs)
+ self.actionSaveInProject().triggered.connect(self.saveInProject)
+ self.actionEditHelp().triggered.connect(self.editHelp)
+ self.actionRun().triggered.connect(self.runModel)
- self.mActionClose.triggered.connect(self.close)
- self.mActionOpen.triggered.connect(self.openModel)
- self.mActionSave.triggered.connect(self.save)
- self.mActionSaveAs.triggered.connect(self.saveAs)
- self.mActionSaveInProject.triggered.connect(self.saveInProject)
- self.mActionZoomIn.triggered.connect(self.zoomIn)
- self.mActionZoomOut.triggered.connect(self.zoomOut)
- self.mActionZoomActual.triggered.connect(self.zoomActual)
- self.mActionZoomToItems.triggered.connect(self.zoomToItems)
- self.mActionExportImage.triggered.connect(self.exportAsImage)
- self.mActionExportPdf.triggered.connect(self.exportAsPdf)
- self.mActionExportSvg.triggered.connect(self.exportAsSvg)
- #self.mActionExportPython.triggered.connect(self.exportAsPython)
- self.mActionEditHelp.triggered.connect(self.editHelp)
- self.mActionRun.triggered.connect(self.runModel)
+ # self.mActionShowComments.toggled.connect(self.showComments)
+ # self.mActionShowComments.setChecked(settings.value("/Processing/stateModeler", self.saveState())))
if model is not None:
- self.model = model.create()
- self.model.setSourceFilePath(model.sourceFilePath())
- self.textGroup.setText(self.model.group())
- self.textName.setText(self.model.displayName())
+ self._model = model.create()
+ self._model.setSourceFilePath(model.sourceFilePath())
+ self.textGroup.setText(self._model.group())
+ self.textName.setText(self._model.displayName())
self.repaintModel()
else:
- self.model = QgsProcessingModelAlgorithm()
- self.model.setProvider(QgsApplication.processingRegistry().providerById('model'))
+ self._model = QgsProcessingModelAlgorithm()
+ self._model.setProvider(QgsApplication.processingRegistry().providerById('model'))
self.update_variables_gui()
self.fillInputsTree()
- self.view.centerOn(0, 0)
+ self.view().centerOn(0, 0)
self.help = None
self.hasChanged = False
def closeEvent(self, evt):
- settings = QgsSettings()
- settings.setValue("/Processing/stateModeler", self.saveState())
- settings.setValue("/Processing/geometryModeler", self.saveGeometry())
-
if self.hasChanged:
ret = QMessageBox.question(
self, self.tr('Save Model?'),
@@ -484,17 +365,20 @@ class ModelerDialog(QgsModelDesignerDialog):
else:
evt.accept()
+ def model(self):
+ return self._model
+
def editHelp(self):
- alg = self.model
+ alg = self.model()
dlg = HelpEditionDialog(alg)
dlg.exec_()
if dlg.descriptions:
- self.model.setHelpContent(dlg.descriptions)
+ self.model().setHelpContent(dlg.descriptions)
self.hasChanged = True
def update_variables_gui(self):
variables_scope = QgsExpressionContextScope(self.tr('Model Variables'))
- for k, v in self.model.variables().items():
+ for k, v in self.model().variables().items():
variables_scope.setVariable(k, v)
variables_context = QgsExpressionContext()
variables_context.appendScope(variables_scope)
@@ -502,19 +386,21 @@ class ModelerDialog(QgsModelDesignerDialog):
self.variables_editor.setEditableScopeIndex(0)
def variables_changed(self):
- self.model.setVariables(self.variables_editor.variablesInActiveScope())
+ self.model().setVariables(self.variables_editor.variablesInActiveScope())
def runModel(self):
- if len(self.model.childAlgorithms()) == 0:
- self.bar.pushMessage("", self.tr("Model doesn't contain any algorithm and/or parameter and can't be executed"), level=Qgis.Warning, duration=5)
+ if len(self.model().childAlgorithms()) == 0:
+ self.messageBar().pushMessage("", self.tr(
+ "Model doesn't contain any algorithm and/or parameter and can't be executed"), level=Qgis.Warning,
+ duration=5)
return
- dlg = AlgorithmDialog(self.model.create(), parent=iface.mainWindow())
- dlg.setParameters(self.model.designerParameterValues())
+ dlg = AlgorithmDialog(self.model().create(), parent=self)
+ dlg.setParameters(self.model().designerParameterValues())
dlg.exec_()
if dlg.wasExecuted():
- self.model.setDesignerParameterValues(dlg.getParameterValues())
+ self.model().setDesignerParameterValues(dlg.getParameterValues())
def save(self):
self.saveModel(False)
@@ -526,161 +412,27 @@ class ModelerDialog(QgsModelDesignerDialog):
if not self.can_save():
return
- self.model.setName(str(self.textName.text()))
- self.model.setGroup(str(self.textGroup.text()))
- self.model.setSourceFilePath(None)
+ self.model().setName(str(self.textName.text()))
+ self.model().setGroup(str(self.textGroup.text()))
+ self.model().setSourceFilePath(None)
project_provider = QgsApplication.processingRegistry().providerById(PROJECT_PROVIDER_ID)
- project_provider.add_model(self.model)
+ project_provider.add_model(self.model())
self.update_model.emit()
- self.bar.pushMessage("", self.tr("Model was saved inside current project"), level=Qgis.Success, duration=5)
+ self.messageBar().pushMessage("", self.tr("Model was saved inside current project"), level=Qgis.Success,
+ duration=5)
self.hasChanged = False
QgsProject.instance().setDirty(True)
- def zoomIn(self):
- self.view.setTransformationAnchor(QGraphicsView.NoAnchor)
- point = self.view.mapToScene(QPoint(self.view.viewport().width() / 2, self.view.viewport().height() / 2))
-
- settings = QgsSettings()
- factor = settings.value('/qgis/zoom_favor', 2.0)
-
- self.view.scale(factor, factor)
- self.view.centerOn(point)
- self.repaintModel()
-
- def zoomOut(self):
- self.view.setTransformationAnchor(QGraphicsView.NoAnchor)
- point = self.view.mapToScene(QPoint(self.view.viewport().width() / 2, self.view.viewport().height() / 2))
-
- settings = QgsSettings()
- factor = settings.value('/qgis/zoom_favor', 2.0)
- factor = 1 / factor
-
- self.view.scale(factor, factor)
- self.view.centerOn(point)
- self.repaintModel()
-
- def zoomActual(self):
- point = self.view.mapToScene(QPoint(self.view.viewport().width() / 2, self.view.viewport().height() / 2))
- self.view.resetTransform()
- self.view.scale(QgsApplication.desktop().logicalDpiX() / 96, QgsApplication.desktop().logicalDpiX() / 96)
- self.view.centerOn(point)
-
- def zoomToItems(self):
- totalRect = self.scene.itemsBoundingRect()
- totalRect.adjust(-10, -10, 10, 10)
- self.view.fitInView(totalRect, Qt.KeepAspectRatio)
-
- def exportAsImage(self):
- self.repaintModel(controls=False)
- filename, fileFilter = QFileDialog.getSaveFileName(self,
- self.tr('Save Model As Image'), '',
- self.tr('PNG files (*.png *.PNG)'))
- if not filename:
- return
-
- if not filename.lower().endswith('.png'):
- filename += '.png'
-
- totalRect = self.scene.itemsBoundingRect()
- totalRect.adjust(-10, -10, 10, 10)
- imgRect = QRectF(0, 0, totalRect.width(), totalRect.height())
-
- img = QImage(totalRect.width(), totalRect.height(),
- QImage.Format_ARGB32_Premultiplied)
- img.fill(Qt.white)
- painter = QPainter()
- painter.setRenderHint(QPainter.Antialiasing)
- painter.begin(img)
- self.scene.render(painter, imgRect, totalRect)
- painter.end()
-
- img.save(filename)
-
- self.bar.pushMessage("", self.tr("Successfully exported model as image to {}").format(QUrl.fromLocalFile(filename).toString(), QDir.toNativeSeparators(filename)), level=Qgis.Success, duration=5)
- self.repaintModel(controls=True)
-
- def exportAsPdf(self):
- self.repaintModel(controls=False)
- filename, fileFilter = QFileDialog.getSaveFileName(self,
- self.tr('Save Model As PDF'), '',
- self.tr('PDF files (*.pdf *.PDF)'))
- if not filename:
- return
-
- if not filename.lower().endswith('.pdf'):
- filename += '.pdf'
-
- totalRect = self.scene.itemsBoundingRect()
- totalRect.adjust(-10, -10, 10, 10)
- printerRect = QRectF(0, 0, totalRect.width(), totalRect.height())
-
- printer = QPrinter()
- printer.setOutputFormat(QPrinter.PdfFormat)
- printer.setOutputFileName(filename)
- printer.setPaperSize(QSizeF(printerRect.width(), printerRect.height()), QPrinter.DevicePixel)
- printer.setFullPage(True)
-
- painter = QPainter(printer)
- self.scene.render(painter, printerRect, totalRect)
- painter.end()
-
- self.bar.pushMessage("", self.tr("Successfully exported model as PDF to {}").format(QUrl.fromLocalFile(filename).toString(), QDir.toNativeSeparators(filename)), level=Qgis.Success, duration=5)
- self.repaintModel(controls=True)
-
- def exportAsSvg(self):
- self.repaintModel(controls=False)
- filename, fileFilter = QFileDialog.getSaveFileName(self,
- self.tr('Save Model As SVG'), '',
- self.tr('SVG files (*.svg *.SVG)'))
- if not filename:
- return
-
- if not filename.lower().endswith('.svg'):
- filename += '.svg'
-
- totalRect = self.scene.itemsBoundingRect()
- totalRect.adjust(-10, -10, 10, 10)
- svgRect = QRectF(0, 0, totalRect.width(), totalRect.height())
-
- svg = QSvgGenerator()
- svg.setFileName(filename)
- svg.setSize(QSize(totalRect.width(), totalRect.height()))
- svg.setViewBox(svgRect)
- svg.setTitle(self.model.displayName())
-
- painter = QPainter(svg)
- self.scene.render(painter, svgRect, totalRect)
- painter.end()
-
- self.bar.pushMessage("", self.tr("Successfully exported model as SVG to {}").format(QUrl.fromLocalFile(filename).toString(), QDir.toNativeSeparators(filename)), level=Qgis.Success, duration=5)
- self.repaintModel(controls=True)
-
- def exportAsPython(self):
- filename, filter = QFileDialog.getSaveFileName(self,
- self.tr('Save Model As Python Script'), '',
- self.tr('Processing scripts (*.py *.PY)'))
- if not filename:
- return
-
- if not filename.lower().endswith('.py'):
- filename += '.py'
-
- text = self.model.asPythonCode()
- with codecs.open(filename, 'w', encoding='utf-8') as fout:
- fout.write(text)
-
- self.bar.pushMessage("", self.tr("Successfully exported model as python script to {}").format(QUrl.fromLocalFile(filename).toString(), QDir.toNativeSeparators(filename)), level=Qgis.Success, duration=5)
-
def can_save(self):
"""
Tests whether a model can be saved, or if it is not yet valid
:return: bool
"""
if str(self.textName.text()).strip() == '':
- self.bar.pushWarning(
+ self.messageBar().pushWarning(
"", self.tr('Please a enter model name before saving')
)
return False
@@ -690,10 +442,10 @@ class ModelerDialog(QgsModelDesignerDialog):
def saveModel(self, saveAs):
if not self.can_save():
return
- self.model.setName(str(self.textName.text()))
- self.model.setGroup(str(self.textGroup.text()))
- if self.model.sourceFilePath() and not saveAs:
- filename = self.model.sourceFilePath()
+ self.model().setName(str(self.textName.text()))
+ self.model().setGroup(str(self.textGroup.text()))
+ if self.model().sourceFilePath() and not saveAs:
+ filename = self.model().sourceFilePath()
else:
filename, filter = QFileDialog.getSaveFileName(self,
self.tr('Save Model'),
@@ -702,23 +454,26 @@ class ModelerDialog(QgsModelDesignerDialog):
if filename:
if not filename.endswith('.model3'):
filename += '.model3'
- self.model.setSourceFilePath(filename)
+ self.model().setSourceFilePath(filename)
if filename:
- if not self.model.toFile(filename):
+ if not self.model().toFile(filename):
if saveAs:
QMessageBox.warning(self, self.tr('I/O error'),
self.tr('Unable to save edits. Reason:\n {0}').format(str(sys.exc_info()[1])))
else:
- QMessageBox.warning(self, self.tr("Can't save model"), QCoreApplication.translate('QgsPluginInstallerInstallingDialog', (
- "This model can't be saved in its original location (probably you do not "
- "have permission to do it). Please, use the 'Save as…' option."))
- )
+ QMessageBox.warning(self, self.tr("Can't save model"),
+ QCoreApplication.translate('QgsPluginInstallerInstallingDialog', (
+ "This model can't be saved in its original location (probably you do not "
+ "have permission to do it). Please, use the 'Save as…' option."))
+ )
return
self.update_model.emit()
if saveAs:
- self.bar.pushMessage("", self.tr("Model was correctly saved to {}").format(QUrl.fromLocalFile(filename).toString(), QDir.toNativeSeparators(filename)), level=Qgis.Success, duration=5)
+ self.messageBar().pushMessage("", self.tr("Model was correctly saved to {}").format(
+ QUrl.fromLocalFile(filename).toString(), QDir.toNativeSeparators(filename)), level=Qgis.Success,
+ duration=5)
else:
- self.bar.pushMessage("", self.tr("Model was correctly saved"), level=Qgis.Success, duration=5)
+ self.messageBar().pushMessage("", self.tr("Model was correctly saved"), level=Qgis.Success, duration=5)
self.hasChanged = False
@@ -733,15 +488,15 @@ class ModelerDialog(QgsModelDesignerDialog):
def loadModel(self, filename):
alg = QgsProcessingModelAlgorithm()
if alg.fromFile(filename):
- self.model = alg
- self.model.setProvider(QgsApplication.processingRegistry().providerById('model'))
+ self._model = alg
+ self._model.setProvider(QgsApplication.processingRegistry().providerById('model'))
self.textGroup.setText(alg.group())
self.textName.setText(alg.name())
self.repaintModel()
self.update_variables_gui()
- self.view.centerOn(0, 0)
+ self.view().centerOn(0, 0)
self.hasChanged = False
else:
QgsMessageLog.logMessage(self.tr('Could not load model {0}').format(filename),
@@ -751,17 +506,17 @@ class ModelerDialog(QgsModelDesignerDialog):
self.tr('The selected model could not be loaded.\n'
'See the log for more information.'))
- def repaintModel(self, controls=True):
+ def repaintModel(self, showControls=True):
self.scene = ModelerScene(self)
self.scene.setSceneRect(QRectF(0, 0, self.CANVAS_SIZE,
self.CANVAS_SIZE))
- if not controls:
+ if not showControls:
self.scene.setFlag(QgsModelGraphicsScene.FlagHideControls)
context = createContext()
- self.scene.createItems(self.model, context)
- self.view.setScene(self.scene)
+ self.scene.createItems(self.model(), context)
+ self.view().setScene(self.scene)
self.scene.rebuildRequired.connect(self.repaintModel)
self.scene.componentChanged.connect(self.componentChanged)
@@ -769,10 +524,10 @@ class ModelerDialog(QgsModelDesignerDialog):
def componentChanged(self):
self.hasChanged = True
- def addInput(self):
+ def _addInput(self):
item = self.inputsTree.currentItem()
param = item.data(0, Qt.UserRole)
- self.addInputOfType(param)
+ self.addInput(param)
def create_widget_context(self):
"""
@@ -782,7 +537,7 @@ class ModelerDialog(QgsModelDesignerDialog):
widget_context.setProject(QgsProject.instance())
if iface is not None:
widget_context.setMapCanvas(iface.mapCanvas())
- widget_context.setModel(self.model)
+ widget_context.setModel(self.model())
return widget_context
def autogenerate_parameter_name(self, parameter):
@@ -795,16 +550,19 @@ class ModelerDialog(QgsModelDesignerDialog):
safeName = ''.join(c for c in parameter.description() if c in validChars)
name = safeName.lower()
i = 2
- while self.model.parameterDefinition(name):
+ while self.model().parameterDefinition(name):
name = safeName.lower() + str(i)
i += 1
parameter.setName(safeName)
- def addInputOfType(self, paramType, pos=None):
+ def addInput(self, paramType, pos=None):
+ if paramType not in [param.id() for param in QgsApplication.instance().processingRegistry().parameterTypes()]:
+ return
+
new_param = None
comment = None
if ModelerParameterDefinitionDialog.use_legacy_dialog(paramType=paramType):
- dlg = ModelerParameterDefinitionDialog(self.model, paramType)
+ dlg = ModelerParameterDefinitionDialog(self.model(), paramType)
if dlg.exec_():
new_param = dlg.param
comment = dlg.comments()
@@ -815,7 +573,7 @@ class ModelerDialog(QgsModelDesignerDialog):
dlg = QgsProcessingParameterDefinitionDialog(type=paramType,
context=context,
widgetContext=widget_context,
- algorithm=self.model)
+ algorithm=self.model())
if dlg.exec_():
new_param = dlg.createParameter()
self.autogenerate_parameter_name(new_param)
@@ -835,17 +593,17 @@ class ModelerDialog(QgsModelDesignerDialog):
component.size().width(),
-1.5 * component.size().height()))
- self.model.addModelParameter(new_param, component)
+ self.model().addModelParameter(new_param, component)
self.repaintModel()
- # self.view.ensureVisible(self.scene.getLastParameterItem())
+ # self.view().ensureVisible(self.scene.getLastParameterItem())
self.hasChanged = True
def getPositionForParameterItem(self):
MARGIN = 20
BOX_WIDTH = 200
BOX_HEIGHT = 80
- if len(self.model.parameterComponents()) > 0:
- maxX = max([i.position().x() for i in list(self.model.parameterComponents().values())])
+ if len(self.model().parameterComponents()) > 0:
+ maxX = max([i.position().x() for i in list(self.model().parameterComponents().values())])
newX = min(MARGIN + BOX_WIDTH + maxX, self.CANVAS_SIZE - BOX_WIDTH)
else:
newX = MARGIN + BOX_WIDTH / 2
@@ -868,14 +626,15 @@ class ModelerDialog(QgsModelDesignerDialog):
self.inputsTree.addTopLevelItem(parametersItem)
parametersItem.setExpanded(True)
- def addAlgorithm(self):
- algorithm = self.algorithmTree.selectedAlgorithm()
- if algorithm is not None:
- alg = QgsApplication.processingRegistry().createAlgorithmById(algorithm.id())
- self._addAlgorithm(alg)
+ def _addAlgorithm(self):
+ self.addAlgorithm(self.algorithmTree.selectedAlgorithm())
- def _addAlgorithm(self, alg, pos=None):
- dlg = ModelerParametersDialog(alg, self.model)
+ def addAlgorithm(self, alg_id, pos=None):
+ alg = QgsApplication.processingRegistry().createAlgorithmById(alg_id)
+ if not alg:
+ return
+
+ dlg = ModelerParametersDialog(alg, self.model())
if dlg.exec_():
alg = dlg.createAlgorithm()
if pos is None:
@@ -893,7 +652,7 @@ class ModelerDialog(QgsModelDesignerDialog):
alg.modelOutput(out).setPosition(alg.position() + QPointF(output_offset_x, output_offset_y))
output_offset_y += 1.5 * alg.modelOutput(out).size().height()
- self.model.addChildAlgorithm(alg)
+ self.model().addChildAlgorithm(alg)
self.repaintModel()
self.hasChanged = True
@@ -901,12 +660,12 @@ class ModelerDialog(QgsModelDesignerDialog):
MARGIN = 20
BOX_WIDTH = 200
BOX_HEIGHT = 80
- if self.model.childAlgorithms():
- maxX = max([alg.position().x() for alg in list(self.model.childAlgorithms().values())])
- maxY = max([alg.position().y() for alg in list(self.model.childAlgorithms().values())])
+ if self.model().childAlgorithms():
+ maxX = max([alg.position().x() for alg in list(self.model().childAlgorithms().values())])
+ maxY = max([alg.position().y() for alg in list(self.model().childAlgorithms().values())])
newX = min(MARGIN + BOX_WIDTH + maxX, self.CANVAS_SIZE - BOX_WIDTH)
- newY = min(MARGIN + BOX_HEIGHT + maxY, self.CANVAS_SIZE -
- BOX_HEIGHT)
+ newY = min(MARGIN + BOX_HEIGHT + maxY, self.CANVAS_SIZE
+ - BOX_HEIGHT)
else:
newX = MARGIN + BOX_WIDTH / 2
newY = MARGIN * 2 + BOX_HEIGHT + BOX_HEIGHT / 2
@@ -915,5 +674,5 @@ class ModelerDialog(QgsModelDesignerDialog):
def export_as_script_algorithm(self):
dlg = ScriptEditorDialog(None)
- dlg.editor.setText('\n'.join(self.model.asPythonCode(QgsProcessing.PythonQgsProcessingAlgorithmSubclass, 4)))
+ dlg.editor.setText('\n'.join(self.model().asPythonCode(QgsProcessing.PythonQgsProcessingAlgorithmSubclass, 4)))
dlg.show()
diff --git a/python/plugins/processing/modeler/OpenModelFromFileAction.py b/python/plugins/processing/modeler/OpenModelFromFileAction.py
index 6771ec82b78..fef6a9ce10a 100644
--- a/python/plugins/processing/modeler/OpenModelFromFileAction.py
+++ b/python/plugins/processing/modeler/OpenModelFromFileAction.py
@@ -26,7 +26,7 @@ from qgis.PyQt.QtWidgets import QFileDialog
from qgis.PyQt.QtCore import QFileInfo, QCoreApplication
from qgis.core import QgsApplication, QgsSettings
-
+from qgis.utils import iface
from processing.gui.ToolboxAction import ToolboxAction
from processing.modeler.ModelerDialog import ModelerDialog
@@ -52,6 +52,6 @@ class OpenModelFromFileAction(ToolboxAction):
settings.setValue('Processing/lastModelsDir',
QFileInfo(filename).absoluteDir().absolutePath())
- dlg = ModelerDialog()
+ dlg = ModelerDialog.create()
dlg.loadModel(filename)
dlg.show()
diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt
index 26f3daf03c7..f7b4e1eb674 100644
--- a/src/gui/CMakeLists.txt
+++ b/src/gui/CMakeLists.txt
@@ -258,6 +258,7 @@ SET(QGIS_GUI_SRCS
processing/models/qgsmodeldesignerdialog.cpp
processing/models/qgsmodelgraphicitem.cpp
processing/models/qgsmodelgraphicsscene.cpp
+ processing/models/qgsmodelgraphicsview.cpp
providers/gdal/qgsgdalsourceselect.cpp
providers/gdal/qgsgdalguiprovider.cpp
@@ -890,6 +891,7 @@ SET(QGIS_GUI_HDRS
processing/models/qgsmodeldesignerdialog.h
processing/models/qgsmodelgraphicitem.h
processing/models/qgsmodelgraphicsscene.h
+ processing/models/qgsmodelgraphicsview.h
providers/gdal/qgsgdalguiprovider.h
providers/gdal/qgsgdalsourceselect.h
diff --git a/src/gui/processing/models/qgsmodeldesignerdialog.cpp b/src/gui/processing/models/qgsmodeldesignerdialog.cpp
index faa816f78d4..67bbdd80596 100644
--- a/src/gui/processing/models/qgsmodeldesignerdialog.cpp
+++ b/src/gui/processing/models/qgsmodeldesignerdialog.cpp
@@ -14,6 +14,21 @@
***************************************************************************/
#include "qgsmodeldesignerdialog.h"
+#include "qgssettings.h"
+#include "qgsapplication.h"
+#include "qgsfileutils.h"
+#include "qgsmessagebar.h"
+#include "qgsprocessingmodelalgorithm.h"
+#include "qgsprocessingregistry.h"
+#include "qgsprocessingalgorithm.h"
+#include "qgsgui.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
///@cond NOT_STABLE
@@ -21,6 +36,181 @@ QgsModelDesignerDialog::QgsModelDesignerDialog( QWidget *parent, Qt::WindowFlags
: QMainWindow( parent, flags )
{
setupUi( this );
+ QgsGui::enableAutoGeometryRestore( this );
+
+ setAttribute( Qt::WA_DeleteOnClose );
+ setWindowFlags( Qt::WindowMinimizeButtonHint |
+ Qt::WindowMaximizeButtonHint |
+ Qt::WindowCloseButtonHint );
+ mMessageBar = new QgsMessageBar();
+ mMessageBar->setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Fixed );
+ mainLayout->insertWidget( 0, mMessageBar );
+
+ mView->setAcceptDrops( true );
+
+ connect( mActionClose, &QAction::triggered, this, &QWidget::close );
+ connect( mActionZoomIn, &QAction::triggered, this, &QgsModelDesignerDialog::zoomIn );
+ connect( mActionZoomOut, &QAction::triggered, this, &QgsModelDesignerDialog::zoomOut );
+ connect( mActionZoomActual, &QAction::triggered, this, &QgsModelDesignerDialog::zoomActual );
+ connect( mActionZoomToItems, &QAction::triggered, this, &QgsModelDesignerDialog::zoomFull );
+ connect( mActionExportImage, &QAction::triggered, this, &QgsModelDesignerDialog::exportToImage );
+ connect( mActionExportPdf, &QAction::triggered, this, &QgsModelDesignerDialog::exportToPdf );
+ connect( mActionExportSvg, &QAction::triggered, this, &QgsModelDesignerDialog::exportToSvg );
+ connect( mActionExportPython, &QAction::triggered, this, &QgsModelDesignerDialog::exportAsPython );
+
+ connect( mView, &QgsModelGraphicsView::algorithmDropped, this, [ = ]( const QString & algorithmId, const QPointF & pos )
+ {
+ addAlgorithm( algorithmId, pos );
+ } );
+ connect( mView, &QgsModelGraphicsView::inputDropped, this, &QgsModelDesignerDialog::addInput );
+
+// Ctrl+= should also trigger a zoom in action
+ QShortcut *ctrlEquals = new QShortcut( QKeySequence( QStringLiteral( "Ctrl+=" ) ), this );
+ connect( ctrlEquals, &QShortcut::activated, this, &QgsModelDesignerDialog::zoomIn );
+}
+
+void QgsModelDesignerDialog::zoomIn()
+{
+ mView->setTransformationAnchor( QGraphicsView::NoAnchor );
+ QPointF point = mView->mapToScene( QPoint( mView->viewport()->width() / 2.0, mView->viewport()->height() / 2 ) );
+ QgsSettings settings;
+ const double factor = settings.value( QStringLiteral( "/qgis/zoom_favor" ), 2.0 ).toDouble();
+ mView->scale( factor, factor );
+ mView->centerOn( point );
+ repaintModel();
+}
+
+void QgsModelDesignerDialog::zoomOut()
+{
+ mView->setTransformationAnchor( QGraphicsView::NoAnchor );
+ QPointF point = mView->mapToScene( QPoint( mView->viewport()->width() / 2.0, mView->viewport()->height() / 2 ) );
+ QgsSettings settings;
+ const double factor = 1.0 / settings.value( QStringLiteral( "/qgis/zoom_favor" ), 2.0 ).toDouble();
+ mView->scale( factor, factor );
+ mView->centerOn( point );
+ repaintModel();
+}
+
+void QgsModelDesignerDialog::zoomActual()
+{
+ QPointF point = mView->mapToScene( QPoint( mView->viewport()->width() / 2.0, mView->viewport()->height() / 2 ) );
+ mView->resetTransform();
+ mView->scale( QgsApplication::desktop()->logicalDpiX() / 96, QgsApplication::desktop()->logicalDpiX() / 96 );
+ mView->centerOn( point );
+}
+
+void QgsModelDesignerDialog::zoomFull()
+{
+ QRectF totalRect = mView->scene()->itemsBoundingRect();
+ totalRect.adjust( -10, -10, 10, 10 );
+ mView->fitInView( totalRect, Qt::KeepAspectRatio );
+}
+
+void QgsModelDesignerDialog::exportToImage()
+{
+ QString filename = QFileDialog::getSaveFileName( this, tr( "Save Model as Image" ), tr( "PNG files (*.png *.PNG)" ) );
+ if ( filename.isEmpty() )
+ return;
+
+ filename = QgsFileUtils::ensureFileNameHasExtension( filename, QStringList() << QStringLiteral( "png" ) );
+
+ repaintModel( false );
+
+ QRectF totalRect = mView->scene()->itemsBoundingRect();
+ totalRect.adjust( -10, -10, 10, 10 );
+ const QRectF imageRect = QRectF( 0, 0, totalRect.width(), totalRect.height() );
+
+ QImage img( totalRect.width(), totalRect.height(),
+ QImage::Format_ARGB32_Premultiplied );
+ img.fill( Qt::white );
+ QPainter painter;
+ painter.setRenderHint( QPainter::Antialiasing );
+ painter.begin( &img );
+ mView->scene()->render( &painter, imageRect, totalRect );
+ painter.end();
+
+ img.save( filename );
+
+ mMessageBar->pushMessage( QString(), tr( "Successfully exported model as image to {}" ).arg( QUrl::fromLocalFile( filename ).toString(), QDir::toNativeSeparators( filename ) ), Qgis::Success, 5 );
+ repaintModel( true );
+}
+
+void QgsModelDesignerDialog::exportToPdf()
+{
+ QString filename = QFileDialog::getSaveFileName( this, tr( "Save Model as PDF" ), tr( "PDF files (*.pdf *.PDF)" ) );
+ if ( filename.isEmpty() )
+ return;
+
+ filename = QgsFileUtils::ensureFileNameHasExtension( filename, QStringList() << QStringLiteral( "pdf" ) );
+
+ repaintModel( false );
+
+ QRectF totalRect = mView->scene()->itemsBoundingRect();
+ totalRect.adjust( -10, -10, 10, 10 );
+ const QRectF printerRect = QRectF( 0, 0, totalRect.width(), totalRect.height() );
+
+ QPrinter printer;
+ printer.setOutputFormat( QPrinter::PdfFormat );
+ printer.setOutputFileName( filename );
+ printer.setPaperSize( QSizeF( printerRect.width(), printerRect.height() ), QPrinter::DevicePixel );
+ printer.setFullPage( true );
+
+ QPainter painter( &printer );
+ mView->scene()->render( &painter, printerRect, totalRect );
+ painter.end();
+
+ mMessageBar->pushMessage( QString(), tr( "Successfully exported model as PDF to {}" ).arg( QUrl::fromLocalFile( filename ).toString(), QDir::toNativeSeparators( filename ) ), Qgis::Success, 5 );
+ repaintModel( true );
+}
+
+void QgsModelDesignerDialog::exportToSvg()
+{
+ QString filename = QFileDialog::getSaveFileName( this, tr( "Save Model as SVG" ), tr( "SVG files (*.svg *.SVG)" ) );
+ if ( filename.isEmpty() )
+ return;
+
+ filename = QgsFileUtils::ensureFileNameHasExtension( filename, QStringList() << QStringLiteral( "svg" ) );
+
+ repaintModel( false );
+
+ QRectF totalRect = mView->scene()->itemsBoundingRect();
+ totalRect.adjust( -10, -10, 10, 10 );
+ const QRectF svgRect = QRectF( 0, 0, totalRect.width(), totalRect.height() );
+
+ QSvgGenerator svg;
+ svg.setFileName( filename );
+ svg.setSize( QSize( totalRect.width(), totalRect.height() ) );
+ svg.setViewBox( svgRect );
+ svg.setTitle( model()->displayName() );
+
+ QPainter painter( &svg );
+ mView->scene()->render( &painter, svgRect, totalRect );
+ painter.end();
+
+ mMessageBar->pushMessage( QString(), tr( "Successfully exported model as SVG to {}" ).arg( QUrl::fromLocalFile( filename ).toString(), QDir::toNativeSeparators( filename ) ), Qgis::Success, 5 );
+ repaintModel( true );
+}
+
+void QgsModelDesignerDialog::exportAsPython()
+{
+ QString filename = QFileDialog::getSaveFileName( this, tr( "Save Model as Python Script" ), tr( "Processing scripts (*.py *.PY)" ) );
+ if ( filename.isEmpty() )
+ return;
+
+ filename = QgsFileUtils::ensureFileNameHasExtension( filename, QStringList() << QStringLiteral( "py" ) );
+
+ const QString text = model()->asPythonCode( QgsProcessing::PythonQgsProcessingAlgorithmSubclass, 4 ).join( '\n' );
+
+ QFile outFile( filename );
+ if ( !outFile.open( QIODevice::WriteOnly | QIODevice::Truncate ) )
+ {
+ return;
+ }
+ QTextStream fout( &outFile );
+ fout << text;
+ outFile.close();
+
+ mMessageBar->pushMessage( QString(), tr( "Successfully exported model as Python script to {}" ).arg( QUrl::fromLocalFile( filename ).toString(), QDir::toNativeSeparators( filename ) ), Qgis::Success, 5 );
}
diff --git a/src/gui/processing/models/qgsmodeldesignerdialog.h b/src/gui/processing/models/qgsmodeldesignerdialog.h
index 99db1c8aec5..8eff83a9eb1 100644
--- a/src/gui/processing/models/qgsmodeldesignerdialog.h
+++ b/src/gui/processing/models/qgsmodeldesignerdialog.h
@@ -20,6 +20,9 @@
#include "qgis_gui.h"
#include "ui_qgsmodeldesignerdialogbase.h"
+class QgsMessageBar;
+class QgsProcessingModelAlgorithm;
+
///@cond NOT_STABLE
/**
@@ -32,11 +35,41 @@ class GUI_EXPORT QgsModelDesignerDialog : public QMainWindow, public Ui::QgsMode
{
public:
- QgsModelDesignerDialog( QWidget *parent = nullptr, Qt::WindowFlags flags = nullptr );
+ QgsModelDesignerDialog( QWidget *parent SIP_TRANSFERTHIS = nullptr, Qt::WindowFlags flags = nullptr );
protected:
+ virtual void repaintModel( bool showControls = true ) = 0;
+ virtual QgsProcessingModelAlgorithm *model() = 0;
+ virtual void addAlgorithm( const QString &algorithmId, const QPointF &pos ) = 0;
+ virtual void addInput( const QString &inputId, const QPointF &pos ) = 0;
+
QToolBar *toolbar() { return mToolbar; }
+ QAction *actionOpen() { return mActionOpen; }
+ QAction *actionSave() { return mActionSave; }
+ QAction *actionSaveAs() { return mActionSaveAs; }
+ QAction *actionSaveInProject() { return mActionSaveInProject; }
+ QAction *actionEditHelp() { return mActionEditHelp; }
+ QAction *actionRun() { return mActionRun; }
+ QAction *actionExportImage() { return mActionExportImage; }
+
+ QgsMessageBar *messageBar() { return mMessageBar; }
+ QGraphicsView *view() { return mView; }
+
+ private slots:
+
+ void zoomIn();
+ void zoomOut();
+ void zoomActual();
+ void zoomFull();
+ void exportToImage();
+ void exportToPdf();
+ void exportToSvg();
+ void exportAsPython();
+
+ private:
+
+ QgsMessageBar *mMessageBar = nullptr;
};
diff --git a/src/gui/processing/models/qgsmodelgraphicsview.cpp b/src/gui/processing/models/qgsmodelgraphicsview.cpp
new file mode 100644
index 00000000000..136c2ac8828
--- /dev/null
+++ b/src/gui/processing/models/qgsmodelgraphicsview.cpp
@@ -0,0 +1,135 @@
+/***************************************************************************
+ qgsmodelgraphicsview.cpp
+ ----------------------------------
+ Date : March 2020
+ Copyright : (C) 2020 Nyall Dawson
+ Email : nyall dot dawson at gmail dot com
+ ***************************************************************************
+ * *
+ * This program is free software; you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation; either version 2 of the License, or *
+ * (at your option) any later version. *
+ * *
+ ***************************************************************************/
+
+#include "qgsmodelgraphicsview.h"
+#include "qgssettings.h"
+
+#include
+#include
+
+///@cond NOT_STABLE
+
+QgsModelGraphicsView::QgsModelGraphicsView( QWidget *parent )
+ : QGraphicsView( parent )
+{
+ setAcceptDrops( true );
+ setDragMode( QGraphicsView::ScrollHandDrag );
+}
+
+void QgsModelGraphicsView::dragEnterEvent( QDragEnterEvent *event )
+{
+ if ( event->mimeData()->hasText() || event->mimeData()->hasFormat( QStringLiteral( "application/x-vnd.qgis.qgis.algorithmid" ) ) )
+ event->acceptProposedAction();
+ else
+ event->ignore();
+}
+
+void QgsModelGraphicsView::dropEvent( QDropEvent *event )
+{
+ const QPointF dropPoint = mapToScene( event->pos() );
+ if ( event->mimeData()->hasFormat( QStringLiteral( "application/x-vnd.qgis.qgis.algorithmid" ) ) )
+ {
+ QByteArray data = event->mimeData()->data( QStringLiteral( "application/x-vnd.qgis.qgis.algorithmid" ) );
+ QDataStream stream( &data, QIODevice::ReadOnly );
+ QString algorithmId;
+ stream >> algorithmId;
+
+ QTimer::singleShot( 0, this, [this, dropPoint, algorithmId ]
+ {
+ emit algorithmDropped( algorithmId, dropPoint );
+ } );
+ event->accept();
+ }
+ else if ( event->mimeData()->hasText() )
+ {
+ const QString itemId = event->mimeData()->text();
+ QTimer::singleShot( 0, this, [this, dropPoint, itemId ]
+ {
+ emit inputDropped( itemId, dropPoint );
+ } );
+ event->accept();
+ }
+ else
+ {
+ event->ignore();
+ }
+}
+
+void QgsModelGraphicsView::dragMoveEvent( QDragMoveEvent *event )
+{
+ if ( event->mimeData()->hasText() || event->mimeData()->hasFormat( QStringLiteral( "application/x-vnd.qgis.qgis.algorithmid" ) ) )
+ event->acceptProposedAction();
+ else
+ event->ignore();
+}
+
+void QgsModelGraphicsView::wheelEvent( QWheelEvent *event )
+{
+ setTransformationAnchor( QGraphicsView::AnchorUnderMouse );
+
+ //get mouse wheel zoom behavior settings
+ QgsSettings settings;
+ double zoomFactor = settings.value( QStringLiteral( "qgis/zoom_factor" ), 2 ).toDouble();
+
+ // "Normal" mouse have an angle delta of 120, precision mouses provide data faster, in smaller steps
+ zoomFactor = 1.0 + ( zoomFactor - 1.0 ) / 120.0 * std::fabs( event->angleDelta().y() );
+
+ if ( event->modifiers() & Qt::ControlModifier )
+ {
+ //holding ctrl while wheel zooming results in a finer zoom
+ zoomFactor = 1.0 + ( zoomFactor - 1.0 ) / 20.0;
+ }
+
+ //calculate zoom scale factor
+ bool zoomIn = event->angleDelta().y() > 0;
+ double scaleFactor = ( zoomIn ? 1 / zoomFactor : zoomFactor );
+
+ scale( scaleFactor, scaleFactor );
+}
+
+void QgsModelGraphicsView::enterEvent( QEvent *event )
+{
+ QGraphicsView::enterEvent( event );
+ viewport()->setCursor( Qt::ArrowCursor );
+}
+
+void QgsModelGraphicsView::mousePressEvent( QMouseEvent *event )
+{
+ if ( event->button() == Qt::MidButton )
+ mPreviousMousePos = event->pos();
+ else
+ QGraphicsView::mousePressEvent( event );
+}
+
+void QgsModelGraphicsView::mouseMoveEvent( QMouseEvent *event )
+{
+ if ( event->buttons() == Qt::MidButton )
+ {
+ const QPoint offset = mPreviousMousePos - event->pos();
+ mPreviousMousePos = event->pos();
+
+ verticalScrollBar()->setValue( verticalScrollBar()->value() + offset.y() );
+ horizontalScrollBar()->setValue( horizontalScrollBar()->value() + offset.x() );
+ }
+ else
+ {
+ QGraphicsView::mouseMoveEvent( event );
+ }
+}
+
+
+///@endcond
+
+
diff --git a/src/gui/processing/models/qgsmodelgraphicsview.h b/src/gui/processing/models/qgsmodelgraphicsview.h
new file mode 100644
index 00000000000..de698065700
--- /dev/null
+++ b/src/gui/processing/models/qgsmodelgraphicsview.h
@@ -0,0 +1,70 @@
+/***************************************************************************
+ qgsmodelgraphicsview.h
+ -----------------------
+ Date : March 2020
+ Copyright : (C) 2020 Nyall Dawson
+ Email : nyall dot dawson at gmail dot com
+ ***************************************************************************
+ * *
+ * This program is free software; you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation; either version 2 of the License, or *
+ * (at your option) any later version. *
+ * *
+ ***************************************************************************/
+
+#ifndef QGSMODELGRAPHICVIEW_H
+#define QGSMODELGRAPHICVIEW_H
+
+#include "qgis.h"
+#include "qgis_gui.h"
+#include "qgsprocessingcontext.h"
+#include
+
+///@cond NOT_STABLE
+
+/**
+ * \ingroup gui
+ * \brief QGraphicsView subclass representing the model designer.
+ * \warning Not stable API
+ * \since QGIS 3.14
+ */
+class GUI_EXPORT QgsModelGraphicsView : public QGraphicsView
+{
+ Q_OBJECT
+
+ public:
+
+ /**
+ * Constructor for QgsModelGraphicsView, with the specified \a parent widget.
+ */
+ QgsModelGraphicsView( QWidget *parent = nullptr );
+
+ void dragEnterEvent( QDragEnterEvent *event ) override;
+ void dropEvent( QDropEvent *event ) override;
+ void dragMoveEvent( QDragMoveEvent *event ) override;
+ void wheelEvent( QWheelEvent *event ) override;
+ void enterEvent( QEvent *event ) override;
+ void mousePressEvent( QMouseEvent *event ) override;
+ void mouseMoveEvent( QMouseEvent *event ) override;
+
+ signals:
+
+ /**
+ * Emitted when an algorithm is dropped onto the view.
+ */
+ void algorithmDropped( const QString &algorithmId, const QPointF &pos );
+
+ /**
+ * Emitted when an input parameter is dropped onto the view.
+ */
+ void inputDropped( const QString &inputId, const QPointF &pos );
+
+ private:
+ QPoint mPreviousMousePos;
+
+};
+
+///@endcond
+
+#endif // QGSMODELGRAPHICVIEW_H
diff --git a/src/ui/processing/qgsmodeldesignerdialogbase.ui b/src/ui/processing/qgsmodeldesignerdialogbase.ui
index 72d0550c24b..30b109c3f24 100644
--- a/src/ui/processing/qgsmodeldesignerdialogbase.ui
+++ b/src/ui/processing/qgsmodeldesignerdialogbase.ui
@@ -14,7 +14,7 @@
Graphical Modeler
-
+
3
@@ -31,7 +31,7 @@
2
-
-
+
@@ -279,8 +279,8 @@
-
- :/icons/default/mActionStartGeoref.png:/icons/default/mActionStartGeoref.png
+
+ :/images/themes/default/mActionStart.svg:/images/themes/default/mActionStart.svg
Run Model…
@@ -318,9 +318,15 @@
+
+
+ QgsModelGraphicsView
+ QGraphicsView
+
+
+
-