mirror of
https://github.com/qgis/QGIS.git
synced 2025-03-06 00:05:02 -05:00
to handle creation of parameter definition widgets Previously, these configuration widgets were all hardcoded into the Python modeler dialog. This prevented 3rd party, plugin provided, parameters from ever being full first class citizens in QGIS, as there was no way to allow their use as inputs to user created models to be customised. Now, the registry is responsible for creating the configuration widget, allowing for 3rd party parameter types to provide their own customised configuration widgets. Refs #26493
920 lines
40 KiB
Python
920 lines
40 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
***************************************************************************
|
|
ModelerDialog.py
|
|
---------------------
|
|
Date : August 2012
|
|
Copyright : (C) 2012 by Victor Olaya
|
|
Email : volayaf 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. *
|
|
* *
|
|
***************************************************************************
|
|
"""
|
|
|
|
__author__ = 'Victor Olaya'
|
|
__date__ = 'August 2012'
|
|
__copyright__ = '(C) 2012, Victor Olaya'
|
|
|
|
import codecs
|
|
import sys
|
|
import operator
|
|
import os
|
|
import warnings
|
|
|
|
from qgis.PyQt import uic
|
|
from qgis.PyQt.QtCore import (
|
|
Qt,
|
|
QCoreApplication,
|
|
QDir,
|
|
QRectF,
|
|
QMimeData,
|
|
QPoint,
|
|
QPointF,
|
|
QByteArray,
|
|
QSize,
|
|
QSizeF,
|
|
pyqtSignal,
|
|
QDataStream,
|
|
QIODevice,
|
|
QUrl,
|
|
QTimer)
|
|
from qgis.PyQt.QtWidgets import (QGraphicsView,
|
|
QTreeWidget,
|
|
QMessageBox,
|
|
QFileDialog,
|
|
QTreeWidgetItem,
|
|
QSizePolicy,
|
|
QMainWindow,
|
|
QShortcut,
|
|
QLabel,
|
|
QDockWidget,
|
|
QWidget,
|
|
QVBoxLayout,
|
|
QGridLayout,
|
|
QFrame,
|
|
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.core import (Qgis,
|
|
QgsApplication,
|
|
QgsProcessing,
|
|
QgsProject,
|
|
QgsSettings,
|
|
QgsMessageLog,
|
|
QgsProcessingUtils,
|
|
QgsProcessingModelAlgorithm,
|
|
QgsProcessingModelParameter,
|
|
QgsProcessingParameterType,
|
|
QgsExpressionContextScope,
|
|
QgsExpressionContext
|
|
)
|
|
from qgis.gui import (QgsMessageBar,
|
|
QgsDockWidget,
|
|
QgsScrollArea,
|
|
QgsFilterLineEdit,
|
|
QgsProcessingToolboxTreeView,
|
|
QgsProcessingToolboxProxyModel,
|
|
QgsProcessingParameterDefinitionDialog,
|
|
QgsVariableEditorWidget,
|
|
QgsProcessingParameterWidgetContext)
|
|
from processing.gui.HelpEditionDialog import HelpEditionDialog
|
|
from processing.gui.AlgorithmDialog import AlgorithmDialog
|
|
from processing.modeler.ModelerParameterDefinitionDialog import ModelerParameterDefinitionDialog
|
|
from processing.modeler.ModelerParametersDialog import ModelerParametersDialog
|
|
from processing.modeler.ModelerUtils import ModelerUtils
|
|
from processing.modeler.ModelerScene import ModelerScene
|
|
from processing.modeler.ProjectProvider import PROJECT_PROVIDER_ID
|
|
from processing.script.ScriptEditorDialog import ScriptEditorDialog
|
|
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]
|
|
with warnings.catch_warnings():
|
|
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
|
WIDGET, BASE = uic.loadUiType(
|
|
os.path.join(pluginPath, 'ui', 'DlgModeler.ui'))
|
|
|
|
|
|
class ModelerToolboxModel(QgsProcessingToolboxProxyModel):
|
|
|
|
def __init__(self, parent=None, registry=None, recentLog=None):
|
|
super().__init__(parent, registry, recentLog)
|
|
|
|
def flags(self, index):
|
|
f = super().flags(index)
|
|
source_index = self.mapToSource(index)
|
|
if self.toolboxModel().isAlgorithm(source_index):
|
|
f = f | Qt.ItemIsDragEnabled
|
|
return f
|
|
|
|
def supportedDragActions(self):
|
|
return Qt.CopyAction
|
|
|
|
|
|
class ModelerDialog(BASE, WIDGET):
|
|
ALG_ITEM = 'ALG_ITEM'
|
|
PROVIDER_ITEM = 'PROVIDER_ITEM'
|
|
GROUP_ITEM = 'GROUP_ITEM'
|
|
|
|
NAME_ROLE = Qt.UserRole
|
|
TAG_ROLE = Qt.UserRole + 1
|
|
TYPE_ROLE = Qt.UserRole + 2
|
|
|
|
CANVAS_SIZE = 4000
|
|
|
|
update_model = pyqtSignal()
|
|
|
|
def __init__(self, model=None):
|
|
super().__init__(None)
|
|
self.setAttribute(Qt.WA_DeleteOnClose)
|
|
|
|
self.setupUi(self)
|
|
|
|
self._variables_scope = None
|
|
|
|
# LOTS of bug reports when we include the dock creation in the UI file
|
|
# see e.g. #16428, #19068
|
|
# So just roll it all by hand......!
|
|
self.propertiesDock = QgsDockWidget(self)
|
|
self.propertiesDock.setFeatures(
|
|
QDockWidget.DockWidgetFloatable | QDockWidget.DockWidgetMovable)
|
|
self.propertiesDock.setObjectName("propertiesDock")
|
|
propertiesDockContents = QWidget()
|
|
self.verticalDockLayout_1 = QVBoxLayout(propertiesDockContents)
|
|
self.verticalDockLayout_1.setContentsMargins(0, 0, 0, 0)
|
|
self.verticalDockLayout_1.setSpacing(0)
|
|
self.scrollArea_1 = QgsScrollArea(propertiesDockContents)
|
|
sizePolicy = QSizePolicy(QSizePolicy.MinimumExpanding,
|
|
QSizePolicy.MinimumExpanding)
|
|
sizePolicy.setHorizontalStretch(0)
|
|
sizePolicy.setVerticalStretch(0)
|
|
sizePolicy.setHeightForWidth(self.scrollArea_1.sizePolicy().hasHeightForWidth())
|
|
self.scrollArea_1.setSizePolicy(sizePolicy)
|
|
self.scrollArea_1.setFocusPolicy(Qt.WheelFocus)
|
|
self.scrollArea_1.setFrameShape(QFrame.NoFrame)
|
|
self.scrollArea_1.setFrameShadow(QFrame.Plain)
|
|
self.scrollArea_1.setWidgetResizable(True)
|
|
self.scrollAreaWidgetContents_1 = QWidget()
|
|
self.gridLayout = QGridLayout(self.scrollAreaWidgetContents_1)
|
|
self.gridLayout.setContentsMargins(6, 6, 6, 6)
|
|
self.gridLayout.setSpacing(4)
|
|
self.label_1 = QLabel(self.scrollAreaWidgetContents_1)
|
|
self.gridLayout.addWidget(self.label_1, 0, 0, 1, 1)
|
|
self.textName = QLineEdit(self.scrollAreaWidgetContents_1)
|
|
self.gridLayout.addWidget(self.textName, 0, 1, 1, 1)
|
|
self.label_2 = QLabel(self.scrollAreaWidgetContents_1)
|
|
self.gridLayout.addWidget(self.label_2, 1, 0, 1, 1)
|
|
self.textGroup = QLineEdit(self.scrollAreaWidgetContents_1)
|
|
self.gridLayout.addWidget(self.textGroup, 1, 1, 1, 1)
|
|
self.label_1.setText(self.tr("Name"))
|
|
self.textName.setToolTip(self.tr("Enter model name here"))
|
|
self.label_2.setText(self.tr("Group"))
|
|
self.textGroup.setToolTip(self.tr("Enter group name here"))
|
|
self.scrollArea_1.setWidget(self.scrollAreaWidgetContents_1)
|
|
self.verticalDockLayout_1.addWidget(self.scrollArea_1)
|
|
self.propertiesDock.setWidget(propertiesDockContents)
|
|
self.propertiesDock.setWindowTitle(self.tr("Model Properties"))
|
|
|
|
self.inputsDock = QgsDockWidget(self)
|
|
self.inputsDock.setFeatures(QDockWidget.DockWidgetFloatable | QDockWidget.DockWidgetMovable)
|
|
self.inputsDock.setObjectName("inputsDock")
|
|
self.inputsDockContents = QWidget()
|
|
self.verticalLayout_3 = QVBoxLayout(self.inputsDockContents)
|
|
self.verticalLayout_3.setContentsMargins(0, 0, 0, 0)
|
|
self.scrollArea_2 = QgsScrollArea(self.inputsDockContents)
|
|
sizePolicy.setHeightForWidth(self.scrollArea_2.sizePolicy().hasHeightForWidth())
|
|
self.scrollArea_2.setSizePolicy(sizePolicy)
|
|
self.scrollArea_2.setFocusPolicy(Qt.WheelFocus)
|
|
self.scrollArea_2.setFrameShape(QFrame.NoFrame)
|
|
self.scrollArea_2.setFrameShadow(QFrame.Plain)
|
|
self.scrollArea_2.setWidgetResizable(True)
|
|
self.scrollAreaWidgetContents_2 = QWidget()
|
|
self.verticalLayout = QVBoxLayout(self.scrollAreaWidgetContents_2)
|
|
self.verticalLayout.setContentsMargins(0, 0, 0, 0)
|
|
self.verticalLayout.setSpacing(0)
|
|
self.inputsTree = QTreeWidget(self.scrollAreaWidgetContents_2)
|
|
self.inputsTree.setAlternatingRowColors(True)
|
|
self.inputsTree.header().setVisible(False)
|
|
self.verticalLayout.addWidget(self.inputsTree)
|
|
self.scrollArea_2.setWidget(self.scrollAreaWidgetContents_2)
|
|
self.verticalLayout_3.addWidget(self.scrollArea_2)
|
|
self.inputsDock.setWidget(self.inputsDockContents)
|
|
self.addDockWidget(Qt.DockWidgetArea(1), self.inputsDock)
|
|
self.inputsDock.setWindowTitle(self.tr("Inputs"))
|
|
|
|
self.algorithmsDock = QgsDockWidget(self)
|
|
self.algorithmsDock.setFeatures(QDockWidget.DockWidgetFloatable | QDockWidget.DockWidgetMovable)
|
|
self.algorithmsDock.setObjectName("algorithmsDock")
|
|
self.algorithmsDockContents = QWidget()
|
|
self.verticalLayout_4 = QVBoxLayout(self.algorithmsDockContents)
|
|
self.verticalLayout_4.setContentsMargins(0, 0, 0, 0)
|
|
self.scrollArea_3 = QgsScrollArea(self.algorithmsDockContents)
|
|
sizePolicy.setHeightForWidth(self.scrollArea_3.sizePolicy().hasHeightForWidth())
|
|
self.scrollArea_3.setSizePolicy(sizePolicy)
|
|
self.scrollArea_3.setFocusPolicy(Qt.WheelFocus)
|
|
self.scrollArea_3.setFrameShape(QFrame.NoFrame)
|
|
self.scrollArea_3.setFrameShadow(QFrame.Plain)
|
|
self.scrollArea_3.setWidgetResizable(True)
|
|
self.scrollAreaWidgetContents_3 = QWidget()
|
|
self.verticalLayout_2 = QVBoxLayout(self.scrollAreaWidgetContents_3)
|
|
self.verticalLayout_2.setContentsMargins(0, 0, 0, 0)
|
|
self.verticalLayout_2.setSpacing(4)
|
|
self.searchBox = QgsFilterLineEdit(self.scrollAreaWidgetContents_3)
|
|
self.verticalLayout_2.addWidget(self.searchBox)
|
|
self.algorithmTree = QgsProcessingToolboxTreeView(None,
|
|
QgsApplication.processingRegistry())
|
|
self.algorithmTree.setAlternatingRowColors(True)
|
|
self.algorithmTree.header().setVisible(False)
|
|
self.verticalLayout_2.addWidget(self.algorithmTree)
|
|
self.scrollArea_3.setWidget(self.scrollAreaWidgetContents_3)
|
|
self.verticalLayout_4.addWidget(self.scrollArea_3)
|
|
self.algorithmsDock.setWidget(self.algorithmsDockContents)
|
|
self.addDockWidget(Qt.DockWidgetArea(1), self.algorithmsDock)
|
|
self.algorithmsDock.setWindowTitle(self.tr("Algorithms"))
|
|
self.searchBox.setToolTip(self.tr("Enter algorithm name to filter list"))
|
|
self.searchBox.setShowSearchIcon(True)
|
|
|
|
self.variables_dock = QgsDockWidget(self)
|
|
self.variables_dock.setFeatures(QDockWidget.DockWidgetFloatable | QDockWidget.DockWidgetMovable)
|
|
self.variables_dock.setObjectName("variablesDock")
|
|
self.variables_dock_contents = QWidget()
|
|
vl_v = QVBoxLayout()
|
|
vl_v.setContentsMargins(0, 0, 0, 0)
|
|
self.variables_editor = QgsVariableEditorWidget()
|
|
vl_v.addWidget(self.variables_editor)
|
|
self.variables_dock_contents.setLayout(vl_v)
|
|
self.variables_dock.setWidget(self.variables_dock_contents)
|
|
self.addDockWidget(Qt.DockWidgetArea(1), self.variables_dock)
|
|
self.variables_dock.setWindowTitle(self.tr("Variables"))
|
|
self.addDockWidget(Qt.DockWidgetArea(1), self.propertiesDock)
|
|
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:
|
|
pass
|
|
|
|
if iface is not None:
|
|
self.mToolbar.setIconSize(iface.iconSize())
|
|
self.setStyleSheet(iface.mainWindow().styleSheet())
|
|
|
|
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.toolbutton_export_to_script.addActions([self.export_to_script_algorithm_action])
|
|
self.mToolbar.insertWidget(self.mActionExportImage, self.toolbutton_export_to_script)
|
|
self.export_to_script_algorithm_action.triggered.connect(self.export_as_script_algorithm)
|
|
|
|
self.mActionOpen.setIcon(
|
|
QgsApplication.getThemeIcon('/mActionFileOpen.svg'))
|
|
self.mActionSave.setIcon(
|
|
QgsApplication.getThemeIcon('/mActionFileSave.svg'))
|
|
self.mActionSaveAs.setIcon(
|
|
QgsApplication.getThemeIcon('/mActionFileSaveAs.svg'))
|
|
self.mActionSaveInProject.setIcon(
|
|
QgsApplication.getThemeIcon('/mAddToProject.svg'))
|
|
self.mActionZoomActual.setIcon(
|
|
QgsApplication.getThemeIcon('/mActionZoomActual.svg'))
|
|
self.mActionZoomIn.setIcon(
|
|
QgsApplication.getThemeIcon('/mActionZoomIn.svg'))
|
|
self.mActionZoomOut.setIcon(
|
|
QgsApplication.getThemeIcon('/mActionZoomOut.svg'))
|
|
self.mActionExportImage.setIcon(
|
|
QgsApplication.getThemeIcon('/mActionSaveMapAsImage.svg'))
|
|
self.mActionZoomToItems.setIcon(
|
|
QgsApplication.getThemeIcon('/mActionZoomFullExtent.svg'))
|
|
self.mActionExportPdf.setIcon(
|
|
QgsApplication.getThemeIcon('/mActionSaveAsPDF.svg'))
|
|
self.mActionExportSvg.setIcon(
|
|
QgsApplication.getThemeIcon('/mActionSaveAsSVG.svg'))
|
|
self.toolbutton_export_to_script.setIcon(
|
|
QgsApplication.getThemeIcon('/mActionSaveAsPython.svg'))
|
|
self.mActionEditHelp.setIcon(
|
|
QgsApplication.getThemeIcon('/mActionEditHelpContent.svg'))
|
|
self.mActionRun.setIcon(
|
|
QgsApplication.getThemeIcon('/mActionStart.svg'))
|
|
|
|
self.addDockWidget(Qt.LeftDockWidgetArea, self.propertiesDock)
|
|
self.addDockWidget(Qt.LeftDockWidgetArea, self.inputsDock)
|
|
self.addDockWidget(Qt.LeftDockWidgetArea, self.algorithmsDock)
|
|
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, dialog=self)
|
|
self.scene.setSceneRect(QRectF(0, 0, self.CANVAS_SIZE, self.CANVAS_SIZE))
|
|
|
|
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
|
|
|
|
def _mimeDataInput(items):
|
|
mimeData = QMimeData()
|
|
text = items[0].data(0, Qt.UserRole)
|
|
mimeData.setText(text)
|
|
return mimeData
|
|
|
|
self.inputsTree.mimeData = _mimeDataInput
|
|
|
|
self.inputsTree.setDragDropMode(QTreeWidget.DragOnly)
|
|
self.inputsTree.setDropIndicatorShown(True)
|
|
|
|
self.algorithms_model = ModelerToolboxModel(self, QgsApplication.processingRegistry())
|
|
self.algorithmTree.setToolboxProxyModel(self.algorithms_model)
|
|
self.algorithmTree.setDragDropMode(QTreeWidget.DragOnly)
|
|
self.algorithmTree.setDropIndicatorShown(True)
|
|
|
|
filters = QgsProcessingToolboxProxyModel.Filters(QgsProcessingToolboxProxyModel.FilterModeler)
|
|
if ProcessingConfig.getSetting(ProcessingConfig.SHOW_ALGORITHMS_KNOWN_ISSUES):
|
|
filters |= QgsProcessingToolboxProxyModel.FilterShowKnownIssues
|
|
self.algorithmTree.setFilters(filters)
|
|
|
|
if hasattr(self.searchBox, 'setPlaceholderText'):
|
|
self.searchBox.setPlaceholderText(QCoreApplication.translate('ModelerDialog', 'Search…'))
|
|
if hasattr(self.textName, 'setPlaceholderText'):
|
|
self.textName.setPlaceholderText(self.tr('Enter model name here'))
|
|
if hasattr(self.textGroup, 'setPlaceholderText'):
|
|
self.textGroup.setPlaceholderText(self.tr('Enter group name here'))
|
|
|
|
# Connect signals and slots
|
|
self.inputsTree.doubleClicked.connect(self.addInput)
|
|
self.searchBox.textChanged.connect(self.algorithmTree.setFilterString)
|
|
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.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)
|
|
|
|
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.repaintModel()
|
|
|
|
else:
|
|
self.model = QgsProcessingModelAlgorithm()
|
|
self.model.setProvider(QgsApplication.processingRegistry().providerById('model'))
|
|
self.update_variables_gui()
|
|
|
|
self.fillInputsTree()
|
|
|
|
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?'),
|
|
self.tr('There are unsaved changes in this model. Do you want to keep those?'),
|
|
QMessageBox.Save | QMessageBox.Cancel | QMessageBox.Discard, QMessageBox.Cancel)
|
|
|
|
if ret == QMessageBox.Save:
|
|
self.saveModel(False)
|
|
evt.accept()
|
|
elif ret == QMessageBox.Discard:
|
|
evt.accept()
|
|
else:
|
|
evt.ignore()
|
|
else:
|
|
evt.accept()
|
|
|
|
def editHelp(self):
|
|
alg = self.model
|
|
dlg = HelpEditionDialog(alg)
|
|
dlg.exec_()
|
|
if 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():
|
|
variables_scope.setVariable(k, v)
|
|
variables_context = QgsExpressionContext()
|
|
variables_context.appendScope(variables_scope)
|
|
self.variables_editor.setContext(variables_context)
|
|
self.variables_editor.setEditableScopeIndex(0)
|
|
|
|
def variables_changed(self):
|
|
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)
|
|
return
|
|
|
|
dlg = AlgorithmDialog(self.model.create(), parent=iface.mainWindow())
|
|
dlg.exec_()
|
|
|
|
def save(self):
|
|
self.saveModel(False)
|
|
|
|
def saveAs(self):
|
|
self.saveModel(True)
|
|
|
|
def saveInProject(self):
|
|
if not self.can_save():
|
|
return
|
|
|
|
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)
|
|
|
|
self.update_model.emit()
|
|
self.bar.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 <a href=\"{}\">{}</a>").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 <a href=\"{}\">{}</a>").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 <a href=\"{}\">{}</a>").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 <a href=\"{}\">{}</a>").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.tr('Please a enter model name before saving')
|
|
)
|
|
return False
|
|
|
|
return True
|
|
|
|
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()
|
|
else:
|
|
filename, filter = QFileDialog.getSaveFileName(self,
|
|
self.tr('Save Model'),
|
|
ModelerUtils.modelsFolders()[0],
|
|
self.tr('Processing models (*.model3 *.MODEL3)'))
|
|
if filename:
|
|
if not filename.endswith('.model3'):
|
|
filename += '.model3'
|
|
self.model.setSourceFilePath(filename)
|
|
if 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."))
|
|
)
|
|
return
|
|
self.update_model.emit()
|
|
if saveAs:
|
|
self.bar.pushMessage("", self.tr("Model was correctly saved to <a href=\"{}\">{}</a>").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.hasChanged = False
|
|
|
|
def openModel(self):
|
|
filename, selected_filter = QFileDialog.getOpenFileName(self,
|
|
self.tr('Open Model'),
|
|
ModelerUtils.modelsFolders()[0],
|
|
self.tr('Processing models (*.model3 *.MODEL3)'))
|
|
if filename:
|
|
self.loadModel(filename)
|
|
|
|
def loadModel(self, filename):
|
|
alg = QgsProcessingModelAlgorithm()
|
|
if alg.fromFile(filename):
|
|
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.hasChanged = False
|
|
else:
|
|
QgsMessageLog.logMessage(self.tr('Could not load model {0}').format(filename),
|
|
self.tr('Processing'),
|
|
Qgis.Critical)
|
|
QMessageBox.critical(self, self.tr('Open Model'),
|
|
self.tr('The selected model could not be loaded.\n'
|
|
'See the log for more information.'))
|
|
|
|
def repaintModel(self, controls=True):
|
|
self.scene = ModelerScene(self, dialog=self)
|
|
self.scene.setSceneRect(QRectF(0, 0, self.CANVAS_SIZE,
|
|
self.CANVAS_SIZE))
|
|
self.scene.paintModel(self.model, controls)
|
|
self.view.setScene(self.scene)
|
|
|
|
def addInput(self):
|
|
item = self.inputsTree.currentItem()
|
|
param = item.data(0, Qt.UserRole)
|
|
self.addInputOfType(param)
|
|
|
|
def create_widget_context(self):
|
|
"""
|
|
Returns a new widget context for use in the model editor
|
|
"""
|
|
widget_context = QgsProcessingParameterWidgetContext()
|
|
widget_context.setProject(QgsProject.instance())
|
|
if iface is not None:
|
|
widget_context.setMapCanvas(iface.mapCanvas())
|
|
widget_context.setModel(self.model)
|
|
return widget_context
|
|
|
|
def autogenerate_parameter_name(self, parameter):
|
|
"""
|
|
Automatically generates and sets a new parameter's name, based on the parameter's
|
|
description and ensuring that it is unique for the model.
|
|
"""
|
|
validChars = \
|
|
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
|
safeName = ''.join(c for c in parameter.description() if c in validChars)
|
|
name = safeName.lower()
|
|
i = 2
|
|
while self.model.parameterDefinition(name):
|
|
name = safeName.lower() + str(i)
|
|
i += 1
|
|
parameter.setName(safeName)
|
|
|
|
def addInputOfType(self, paramType, pos=None):
|
|
new_param = None
|
|
if ModelerParameterDefinitionDialog.use_legacy_dialog(paramType=paramType):
|
|
dlg = ModelerParameterDefinitionDialog(self.model, paramType)
|
|
if dlg.exec_():
|
|
new_param = dlg.param
|
|
else:
|
|
# yay, use new API!
|
|
context = createContext()
|
|
widget_context = self.create_widget_context()
|
|
dlg = QgsProcessingParameterDefinitionDialog(type=paramType,
|
|
context=context,
|
|
widgetContext=widget_context,
|
|
algorithm=self.model)
|
|
if dlg.exec_():
|
|
new_param = dlg.createParameter()
|
|
self.autogenerate_parameter_name(new_param)
|
|
|
|
if new_param is not None:
|
|
if pos is None:
|
|
pos = self.getPositionForParameterItem()
|
|
if isinstance(pos, QPoint):
|
|
pos = QPointF(pos)
|
|
component = QgsProcessingModelParameter(new_param.name())
|
|
component.setDescription(new_param.name())
|
|
component.setPosition(pos)
|
|
self.model.addModelParameter(new_param, component)
|
|
self.repaintModel()
|
|
# 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())])
|
|
newX = min(MARGIN + BOX_WIDTH + maxX, self.CANVAS_SIZE - BOX_WIDTH)
|
|
else:
|
|
newX = MARGIN + BOX_WIDTH / 2
|
|
return QPointF(newX, MARGIN + BOX_HEIGHT / 2)
|
|
|
|
def fillInputsTree(self):
|
|
icon = QIcon(os.path.join(pluginPath, 'images', 'input.svg'))
|
|
parametersItem = QTreeWidgetItem()
|
|
parametersItem.setText(0, self.tr('Parameters'))
|
|
sortedParams = sorted(QgsApplication.instance().processingRegistry().parameterTypes(), key=lambda pt: pt.name())
|
|
for param in sortedParams:
|
|
if param.flags() & QgsProcessingParameterType.ExposeToModeler:
|
|
paramItem = QTreeWidgetItem()
|
|
paramItem.setText(0, param.name())
|
|
paramItem.setData(0, Qt.UserRole, param.id())
|
|
paramItem.setIcon(0, icon)
|
|
paramItem.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled)
|
|
paramItem.setToolTip(0, param.description())
|
|
parametersItem.addChild(paramItem)
|
|
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, alg, pos=None):
|
|
dlg = ModelerParametersDialog(alg, self.model)
|
|
if dlg.exec_():
|
|
alg = dlg.createAlgorithm()
|
|
if pos is None:
|
|
alg.setPosition(self.getPositionForAlgorithmItem())
|
|
else:
|
|
alg.setPosition(pos)
|
|
from processing.modeler.ModelerGraphicItem import ModelerGraphicItem
|
|
for i, out in enumerate(alg.modelOutputs()):
|
|
alg.modelOutput(out).setPosition(alg.position() + QPointF(ModelerGraphicItem.BOX_WIDTH, (i + 1.5) *
|
|
ModelerGraphicItem.BOX_HEIGHT))
|
|
self.model.addChildAlgorithm(alg)
|
|
self.repaintModel()
|
|
self.hasChanged = True
|
|
|
|
def getPositionForAlgorithmItem(self):
|
|
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())])
|
|
newX = min(MARGIN + BOX_WIDTH + maxX, self.CANVAS_SIZE - BOX_WIDTH)
|
|
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
|
|
return QPointF(newX, newY)
|
|
|
|
def export_as_script_algorithm(self):
|
|
dlg = ScriptEditorDialog(None)
|
|
|
|
dlg.editor.setText('\n'.join(self.model.asPythonCode(QgsProcessing.PythonQgsProcessingAlgorithmSubclass, 4)))
|
|
dlg.show()
|