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 +
qgsmodelgraphicsview.h
+
+
-