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 addInputOfType(self, paramType, pos=None): dlg = ModelerParameterDefinitionDialog(self.model, paramType) dlg.exec_() if dlg.param is not None: if pos is None: pos = self.getPositionForParameterItem() if isinstance(pos, QPoint): pos = QPointF(pos) component = QgsProcessingModelParameter(dlg.param.name()) component.setDescription(dlg.param.name()) component.setPosition(pos) self.model.addModelParameter(dlg.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()
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) # 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.addDockWidget(Qt.DockWidgetArea(1), self.propertiesDock) 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 = QTreeWidget(self.scrollAreaWidgetContents_3) 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.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 self.mToolbar.setIconSize(iface.iconSize()) self.mActionOpen.setIcon( QgsApplication.getThemeIcon('/mActionFileOpen.svg')) self.mActionSave.setIcon( QgsApplication.getThemeIcon('/mActionFileSave.svg')) self.mActionSaveAs.setIcon( QgsApplication.getThemeIcon('/mActionFileSaveAs.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.mActionExportPython.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.zoom = 1 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) def _dragEnterEvent(event): if event.mimeData().hasText(): event.acceptProposedAction() else: event.ignore() def _dropEvent(event): if event.mimeData().hasText(): itemId = event.mimeData().text() if itemId in [param.id() for param in QgsApplication.instance().processingRegistry().parameterTypes()]: self.addInputOfType(itemId, event.pos()) else: alg = QgsApplication.processingRegistry().createAlgorithmById(itemId) if alg is not None: self._addAlgorithm(alg, event.pos()) event.accept() else: event.ignore() def _dragMoveEvent(event): if event.mimeData().hasText(): 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) def _mimeDataAlgorithm(items): item = items[0] mimeData = None if isinstance(item, TreeAlgorithmItem): mimeData = QMimeData() mimeData.setText(item.alg.id()) return mimeData self.algorithmTree.mimeData = _mimeDataAlgorithm self.algorithmTree.setDragDropMode(QTreeWidget.DragOnly) self.algorithmTree.setDropIndicatorShown(True) 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.textChanged) 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.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.fillInputsTree() self.fillTreeUsingProviders() 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 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) dlg.exec_() def save(self): self.saveModel(False) def saveAs(self): self.saveModel(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.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("Model was correctly exported as image"), 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("Model was correctly exported as PDF"), 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("Model was correctly exported as SVG"), 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('Python files (*.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("Model was correctly exported as python script"), level=Qgis.Success, duration=5) def saveModel(self, saveAs): if str(self.textGroup.text()).strip() == '' \ or str(self.textName.text()).strip() == '': QMessageBox.warning( self, self.tr('Warning'), self.tr('Please enter group and model names before saving') ) 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)')) 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() 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.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 addInputOfType(self, paramType, pos=None): dlg = ModelerParameterDefinitionDialog(self.model, paramType) dlg.exec_() if dlg.param is not None: if pos is None: pos = self.getPositionForParameterItem() if isinstance(pos, QPoint): pos = QPointF(pos) component = QgsProcessingModelParameter(dlg.param.name()) component.setDescription(dlg.param.name()) component.setPosition(pos) self.model.addModelParameter(dlg.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 textChanged(self): text = self.searchBox.text().strip(' ').lower() for item in list(self.disabledProviderItems.values()): item.setHidden(True) self._filterItem(self.algorithmTree.invisibleRootItem(), [t for t in text.split(' ') if t]) if text: self.algorithmTree.expandAll() self.disabledWithMatchingAlgs = [] for provider in QgsApplication.processingRegistry().providers(): if not provider.isActive(): for alg in provider.algorithms(): if text in alg.name(): self.disabledWithMatchingAlgs.append(provider.id()) break else: self.algorithmTree.collapseAll() def _filterItem(self, item, text): if (item.childCount() > 0): show = False for i in range(item.childCount()): child = item.child(i) showChild = self._filterItem(child, text) show = (showChild or show) and item not in list(self.disabledProviderItems.values()) item.setHidden(not show) return show elif isinstance(item, (TreeAlgorithmItem, TreeActionItem)): # hide if every part of text is not contained somewhere in either the item text or item user role item_text = [item.text(0).lower(), item.data(0, ModelerDialog.NAME_ROLE).lower()] if isinstance(item, TreeAlgorithmItem): item_text.append(item.alg.id().lower()) if item.alg.shortDescription(): item_text.append(item.alg.shortDescription().lower()) item_text.extend([t.lower() for t in item.data(0, ModelerDialog.TAG_ROLE)]) hide = bool(text) and not all( any(part in t for t in item_text) for part in text) item.setHidden(hide) return not hide else: item.setHidden(True) return False 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): item = self.algorithmTree.currentItem() if isinstance(item, TreeAlgorithmItem): alg = QgsApplication.processingRegistry().createAlgorithmById(item.alg.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 fillTreeUsingProviders(self): self.algorithmTree.clear() self.disabledProviderItems = {} # TODO - replace with proper model for toolbox! # first add qgis/native providers, since they create top level groups for provider in QgsApplication.processingRegistry().providers(): if provider.id() in ('qgis', 'native', '3d'): self.addAlgorithmsFromProvider(provider, self.algorithmTree.invisibleRootItem()) else: continue self.algorithmTree.sortItems(0, Qt.AscendingOrder) for provider in QgsApplication.processingRegistry().providers(): if provider.id() in ('qgis', 'native', '3d'): # already added continue else: providerItem = TreeProviderItem(provider, self.algorithmTree, self) # insert non-native providers at end of tree, alphabetically for i in range(self.algorithmTree.invisibleRootItem().childCount()): child = self.algorithmTree.invisibleRootItem().child(i) if isinstance(child, TreeProviderItem): if child.text(0) > providerItem.text(0): break self.algorithmTree.insertTopLevelItem(i + 1, providerItem) if not provider.isActive(): providerItem.setHidden(True) self.disabledProviderItems[provider.id()] = providerItem def addAlgorithmsFromProvider(self, provider, parent): groups = {} count = 0 algs = provider.algorithms() active = provider.isActive() # Add algorithms for alg in algs: if alg.flags() & QgsProcessingAlgorithm.FlagHideFromModeler: continue groupItem = None if alg.group() in groups: groupItem = groups[alg.group()] else: # check if group already exists for i in range(parent.childCount()): if parent.child(i).text(0) == alg.group(): groupItem = parent.child(i) groups[alg.group()] = groupItem break if not groupItem: groupItem = TreeGroupItem(alg.group()) if not active: groupItem.setInactive() if provider.id() in ('qgis', 'native', '3d'): groupItem.setIcon(0, provider.icon()) groups[alg.group()] = groupItem algItem = TreeAlgorithmItem(alg) if not active: algItem.setForeground(0, Qt.darkGray) groupItem.addChild(algItem) count += 1 text = provider.name() if not provider.id() in ('qgis', 'native', '3d'): if not active: def activateProvider(): self.activateProvider(provider.id()) label = QLabel(text + " <a href='%s'>Activate</a>") label.setStyleSheet("QLabel {background-color: white; color: grey;}") label.linkActivated.connect(activateProvider) self.algorithmTree.setItemWidget(parent, 0, label) else: parent.setText(0, text) for group, groupItem in sorted(groups.items(), key=operator.itemgetter(1)): parent.addChild(groupItem) if not provider.id() in ('qgis', 'native', '3d'): parent.setHidden(parent.childCount() == 0)
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) # 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.addDockWidget(Qt.DockWidgetArea(1), self.propertiesDock) 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.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.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.mActionExportPython.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.zoom = 1 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) 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): 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() alg = QgsApplication.processingRegistry().createAlgorithmById(algorithm_id) if alg is not None: self._addAlgorithm(alg, event.pos()) else: assert False, algorithm_id elif event.mimeData().hasText(): itemId = event.mimeData().text() if itemId in [param.id() for param in QgsApplication.instance().processingRegistry().parameterTypes()]: self.addInputOfType(itemId, event.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) self.algorithmTree.setFilters(QgsProcessingToolboxProxyModel.FilterModeler) 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.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 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) 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.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.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 addInputOfType(self, paramType, pos=None): dlg = ModelerParameterDefinitionDialog(self.model, paramType) dlg.exec_() if dlg.param is not None: if pos is None: pos = self.getPositionForParameterItem() if isinstance(pos, QPoint): pos = QPointF(pos) component = QgsProcessingModelParameter(dlg.param.name()) component.setDescription(dlg.param.name()) component.setPosition(pos) self.model.addModelParameter(dlg.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)
class GISCloudPublisher(QObject): """This class controls all plugin-related GUI elements.""" def __init__(self, iface): QObject.__init__(self) self.iface = iface self.path = os.path.dirname(os.path.abspath(__file__)) self.check = None self.read_project = iface.projectRead self.new_project = iface.newProjectCreated self.main_widget = None self.main_widget_opened = False self.current_dock_widget = None self.previous_dock_widget = None self.objects_with_connected_signals = [] self.is_initialized = False self.qgis_api = None self.api = None self.message_box = None self.login_control = None self.publish_control = None self.update_control = None self.check_task = None self.map_analysis_task = None self.analysis_timer = None self.action = None self.gui_initialized = False self.default_input_style_dark = 'QLineEdit\ {border:0px;\ border-radius:2px;\ padding:0px 8px 0px 8px;\ color:white;}' self.default_input_style_dark_error = 'QLineEdit\ {border:1px solid red;\ border-radius: 2px;\ padding:0px 8px 0px 8px;\ color:white;}' self.default_input_style_lite = 'QLineEdit\ {border:0px;\ border-radius:2px;\ padding:0px 8px 0px 8px;\ color:#222222} ' self.default_input_style_lite_error = 'QLineEdit\ {border:1px solid red;\ border-radius: 2px;\ padding:0px 8px 0px 8px;\ color:#222222}' self.layer_hooks = [ "styleChanged", "blendModeChanged", "configChanged", "crsChanged", "legendChanged", "metadataChanged", "nameChanged", "statusChanged", "rendererChanged", "repaintRequested", "flagsChanged" ] def initGui(self): """initialize the gui""" # pylint: disable=C0103 self.gui_initialized = True self.qgis_api = GISCloudQgisCore(self.path) self.api = GISCloudCore(self.path, self.qgis_api, self) self.qgis_api.gc_api = self.api self.message_box = QMessageBox() self.login_control = GISCloudUiLogin(self) self.publish_control = GISCloudUiPublish(self) self.update_control = GISCloudUiUpdate(self) self.adjust_theme(self.login_control.login_dock.username, True) self.adjust_theme(self.login_control.login_dock.password, True) self.adjust_theme(self.publish_control.publish_details_dock.map_name, True) if ISQGIS3: components = (self.login_control.login_dock.username, self.login_control.login_dock.password, self.publish_control.publish_details_dock.map_name) for component in components: component.installEventFilter(self) self.check_task = GISCloudWorkerCheckConnection( self.api, self.qgis_api) self.check_task.alertSignal.connect(self.__notify_user_publish) self.check_task.wrongApiSignal.connect(self.__wrong_api_notify) self.map_analysis_task = GISCloudWorkerMapAnalysis(self.qgis_api) if ISQGIS3: self.map_analysis_task.result.connect( self.__handle_project_update_test) self.analysis_timer = QTimer() self.analysis_timer.timeout.connect(self.__run_analysis) self.main_widget = QgsDockWidget("Publisher") self.main_widget.setFixedSize(QSize(360, 310)) self.main_widget.setObjectName("GISCloudPublisherMainDockWidget") self.login_control.is_auth = self.api.user.is_auth_api() if self.login_control.is_auth: self.set_dock_widget(self.publish_control.publish_dock) else: self.set_dock_widget(self.login_control.login_dock) if ISQGIS3: self.login_control.login_dock.username.setFocus() self.action = QAction( QIcon(":/plugins/gis_cloud_publisher/ui/img/icon.png"), "GIS Cloud Publisher", self.iface.mainWindow()) self.action.triggered.connect(self.__toggle_dock) self.iface.addPluginToWebMenu("&GIS Cloud Publisher", self.action) self.main_widget.opened.connect(self.__dock_widget_opened) self.main_widget.closed.connect(self.__dock_widget_closed) self.iface.addDockWidget(Qt.LeftDockWidgetArea, self.main_widget) def eventFilter(self, obj, event): """This is needed to filter events and hook on palette change, to detect lite/dark mode switches""" # pylint: disable=C0103 if event.type() == QEvent.PaletteChange: self.adjust_theme(obj) return True return QObject.eventFilter(self, obj, event) def adjust_theme(self, obj, force=False): """We adjust theme for input field when dark/lite theme is selected.""" color = self.iface.mainWindow().palette().window().color() color_condition = color.red() <= 50 and \ color.green() <= 50 and \ color.blue() <= 50 if color_condition and (force or not obj.darkMode): obj.darkMode = True obj.default_input_style = self.default_input_style_dark obj.default_input_style_error = self.default_input_style_dark_error obj.setStyleSheet(self.default_input_style_dark) elif not color_condition and (force or obj.darkMode): obj.darkMode = False obj.default_input_style = self.default_input_style_lite obj.default_input_style_error = self.default_input_style_lite_error obj.setStyleSheet(self.default_input_style_lite) def logged_in(self): """Login control calls this method once it has logged in""" self.set_dock_widget(self.publish_control.publish_dock) self.set_login_info(self.publish_control.publish_dock.user) LOGGER.info("Login has been succesful") self.__project_check() def notify_user_login(self, handler): """Notify user if somethhing goes wrong during login.""" # pylint: disable=W0613 self.login_control.animation.stop() self.__no_internet_message() self.restore_previous_dock_widget() def set_login_info(self, label): """Set login information in a widgets""" label.setWordWrap(True) label.setText("You are logged in as <b>" + str(self.api.user.username) + "</b>") def published(self): """Publish control will call this method once publish has been completed""" self.check = True self.update_control.get_map_link() self.__project_check() # if project is modified or newly created, prompt user to save it if (self.qgis_api.project.isDirty() or not self.qgis_api.project.fileName() and GISCloudQgisUtils.get_qgis_layers(self.qgis_api.project)): save = self.message_box.information( self.iface.mainWindow(), 'Information', 'To be able to update this QGIS project later and ' + 'sync it with published GIS Cloud map, please save it now.', self.message_box.Save, self.message_box.Cancel) if save == self.message_box.Save: # have to save project like this, # otherwise qgis doesn't detect that # project has been saved self.iface.mainWindow().findChild( QAction, 'mActionSaveProject').trigger() def deleted_map_message(self): """If user has deleted map on GIS Cloud update should fail and warn about this.""" self.message_box.warning( self.iface.mainWindow(), 'Warning', "The published version of this map " + "was deleted from GIS Cloud, " + "so it can't be updated. " + "You can publish your map again.", self.message_box.Ok, self.message_box.Ok) self.__project_check() def publishing_cancel(self): """Cancel publish process and return to Publish or Update Dock.""" self.publish_control.sync_task.quit() self.publish_control.publish_write_state() if self.check: self.set_dock_widget(self.update_control.update_done_dock) else: self.set_dock_widget(self.publish_control.publish_dock) self.__project_check() def set_dock_widget(self, widget): """Helper method that makes it easy to switch widgets.""" if self.current_dock_widget == widget or not self.gui_initialized: return self.previous_dock_widget = self.current_dock_widget self.current_dock_widget = widget self.main_widget.setWidget(widget) self.main_widget.show() def restore_previous_dock_widget(self): """Helper method that makes it easy to go back to a previous widget.""" if self.previous_dock_widget: self.set_dock_widget(self.previous_dock_widget) def inform_fail(self, layer, error_details): """Inform user that upload of certain layer has failed.""" if error_details == "Storage limit exceeded": message = "Project wasn't published because you exceeded " + \ "your GIS Cloud storage limit.\n\n" + \ "Go to Store in the GIS Cloud Manager " + \ "and buy morestorage to continue. " + \ "If the issue persists, contact [email protected]." else: error_details = ': ' + error_details if error_details else \ ', we are sorry for the incovenience' if layer: message = ( 'Project layer \"{0}\" couldn\'t be uploaded ' + 'correctly{1}.\n\nPlease set the layer \"{0}\" to ' + '\'not visible\' in your layer list and try again.\n\n') \ .format(layer.name, error_details) else: message = 'Project couldn\'t be uploaded correctly{0}.\n\n' \ .format(error_details) message += 'If the issue persists, contact [email protected].' failed = self.message_box.critical(self.iface.mainWindow(), 'Error', message, self.message_box.Ok) if failed == self.message_box.Ok: self.publish_control.sync_task.quit() self.__project_check() def handle_project_update(self): """Starting the analysis with a 50msec throttle""" if self.login_control.is_auth: self.analysis_timer.start(50) def __dock_widget_opened(self): """Handling widget open""" if self.main_widget_opened: return self.main_widget_opened = True self.read_project.connect(self.__load_project) self.new_project.connect(self.__load_project) if self.login_control.is_auth and not self.api.user.username: self.api.user.get_username(self.__ui_init) else: self.__ui_init() self.__load_project(True) def __dock_widget_closed(self): """Handling widget close""" if not self.main_widget_opened: return self.main_widget_opened = False self.read_project.disconnect(self.__load_project) self.new_project.disconnect(self.__load_project) self.__unhook_all_events() def unload(self): """Unload the plugin.""" self.gui_initialized = False GISCloudNetworkHandler.cancel() gc_publisher_loggers_unload() self.__dock_widget_closed() self.main_widget.opened.disconnect(self.__dock_widget_opened) self.main_widget.closed.disconnect(self.__dock_widget_closed) self.iface.removeDockWidget(self.main_widget) self.current_dock_widget = None self.iface.removePluginWebMenu("GIS Cloud Publisher", self.action) def __ui_init(self): """Final stage of UI initialization.""" self.is_initialized = True if not self.login_control.is_auth or not self.api.user.username: self.login_control.is_auth = None self.set_dock_widget(self.login_control.login_dock) if ISQGIS3: self.login_control.login_dock.username.setFocus() else: self.set_dock_widget(self.publish_control.publish_dock) self.set_login_info(self.publish_control.publish_dock.user) self.__project_check() def __handle_added_child(self, group): if GISCloudQgisUtils.is_layer_tree_node(group): return for node in group.children(): if GISCloudQgisUtils.is_layer_tree_node(node): self.__hook_on_layer_events([node.layer()]) def __hook_on_layer_legend(self): root = self.qgis_api.project.layerTreeRoot() if root in self.objects_with_connected_signals: return self.objects_with_connected_signals.append(root) if not ISQGIS3: root.addedChildren.connect(self.__handle_added_child) return if root.layerOrderChanged: root.layerOrderChanged.connect(self.handle_project_update) if root.visibilityChanged: root.visibilityChanged.connect(self.handle_project_update) if root.nameChanged: root.nameChanged.connect(self.handle_project_update) def __unhook_on_layer_legend(self): root = self.qgis_api.project.layerTreeRoot() if root not in self.objects_with_connected_signals: return if not ISQGIS3: root.addedChildren.disconnect(self.__handle_added_child) return if root.layerOrderChanged: root.layerOrderChanged.disconnect(self.handle_project_update) if root.visibilityChanged: root.visibilityChanged.disconnect(self.handle_project_update) if root.nameChanged: root.nameChanged.disconnect(self.handle_project_update) def __hook_on_layer_events(self, layers): for layer in layers: if layer is None or layer in self.objects_with_connected_signals: continue self.objects_with_connected_signals.append(layer) event_handler = GISCloudQgisLayerEvent(self, layer) layer.giscloud_event_handler = event_handler layer.dataChanged.connect(event_handler.handle_data_change) layer.dataProvider().dataChanged.connect( event_handler.handle_data_change) if GISCloudQgisUtils.is_vector_layer(layer): layer.editingStopped.connect(event_handler.handle_data_change) if not ISQGIS3: continue if hasattr(layer, "dataSourceChanged"): layer.dataSourceChanged.connect( event_handler.handle_data_change) for hook in self.layer_hooks: if hasattr(layer, hook): signal = getattr(layer, hook) signal.connect(self.handle_project_update) def __unhook_on_layer_events(self, layers): for layer in layers: if layer not in self.objects_with_connected_signals: continue event_handler = layer.giscloud_event_handler layer.dataChanged.disconnect(event_handler.handle_data_change) layer.dataProvider().dataChanged.disconnect( event_handler.handle_data_change) if GISCloudQgisUtils.is_vector_layer(layer): layer.editingStopped.disconnect( event_handler.handle_data_change) if not ISQGIS3: continue if hasattr(layer, "dataSourceChanged"): layer.dataSourceChanged.disconnect( event_handler.handle_data_change) for hook in self.layer_hooks: if hasattr(layer, hook): signal = getattr(layer, hook) signal.disconnect(self.handle_project_update) def __unhook_all_events(self, unhook_layer_events=True): if unhook_layer_events or ISQGIS3: self.__unhook_on_layer_events( GISCloudQgisUtils.get_qgis_layers(self.qgis_api.project)) self.__unhook_on_layer_legend() self.objects_with_connected_signals = [] def __load_project(self, unhook_layer_events=False): self.qgis_api.init_project() self.__unhook_all_events(unhook_layer_events) self.__hook_on_layer_legend() self.__hook_on_layer_events( GISCloudQgisUtils.get_qgis_layers(self.qgis_api.project)) if ISQGIS3: self.qgis_api.project.legendLayersAdded.connect( self.__hook_on_layer_events) if self.is_initialized: self.__project_check() def __project_check(self): """Check if loaded project is already synced with GIS Cloud.""" self.qgis_api.map_name_override = None self.api.map.map_name = self.qgis_api.get_map_name(True) if self.qgis_api.project.readEntry("giscloud_project", "save_as")[0] != '': map_id = int( self.qgis_api.project.readEntry("giscloud_project", "save_as")[0]) self.api.map.map_id = map_id else: self.api.map.map_id = None if not self.publish_control.sync_task.isRunning(): if self.login_control.is_auth: if self.api.map.map_id: self.check = True map_name = self.qgis_api.project.readEntry( 'giscloud_map_name', 'name')[0] self.qgis_api.map_name_override = map_name self.api.map.map_name = map_name layer_data_timestamps = self.qgis_api.project.readEntry( 'giscloud_layers_data_state', 'state')[0] self.qgis_api.layer_data_timestamps = json.loads( layer_data_timestamps ) if layer_data_timestamps else {} if not ISQGIS3: self.set_dock_widget(self.update_control.update_dock) self.set_login_info( self.update_control.update_dock.user) else: self.set_dock_widget( self.update_control.update_done_dock) self.set_login_info( self.update_control.update_done_dock.user) self.update_control.get_map_link() else: self.set_dock_widget(self.publish_control.publish_dock) self.check = False LOGGER.info("Map name: {}".format(self.api.map.map_name)) LOGGER.info("Map id: {}".format(self.api.map.map_id)) self.handle_project_update() def __notify_user_publish(self): """Notify user if internet connection is bad.""" self.publishing_cancel() self.check_task.terminate() self.__no_internet_message() def __no_internet_message(self): """Warn user that his internet connection is too slow or non existent.""" notify = self.message_box.warning( self.iface.mainWindow(), 'Warning', 'No internet connection. ' + 'It looks like you are not connected to the internet. ' + 'Please check your internet connection and try again.', self.message_box.Ok) if notify == self.message_box.Ok: self.message_box.close() def __wrong_api_notify(self): """Notifiy user if api key has been deleted.""" wrong_api = True self.check_task.terminate() wrong_api = self.message_box.warning( self.iface.mainWindow(), 'Warning', 'It seems that you have deleted your api key. ' + 'Please log out in order to refresh api key.', self.message_box.Ok) if wrong_api == self.message_box.Ok: self.message_box.close() def __toggle_dock(self): if self.main_widget.isUserVisible(): self.main_widget.close() else: self.main_widget.show() def __handle_project_update_test(self, result): invalidated = result["layers_changes"] > 0 or \ result["folders_changes"] > 0 or \ result["layers_to_delete"] > 0 if invalidated and \ self.current_dock_widget == \ self.update_control.update_done_dock: self.set_dock_widget(self.update_control.update_dock) self.set_login_info(self.update_control.update_dock.user) if not invalidated and \ self.current_dock_widget == \ self.update_control.update_dock: self.set_dock_widget(self.update_control.update_done_dock) self.set_login_info(self.update_control.update_done_dock.user) def __run_analysis(self): """Running layer analysis with a throttle.""" self.analysis_timer.stop() self.map_analysis_task.start()