# -*- coding: utf-8 -*- """ *************************************************************************** ModelerGraphicItem.py --------------------- Date : August 2012 Copyright : (C) 2012 by Victor Olaya Email : volayaf at gmail dot com *************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * *************************************************************************** """ __author__ = 'Victor Olaya' __date__ = 'August 2012' __copyright__ = '(C) 2012, Victor Olaya' # This will get replaced with a git SHA1 when you do a git archive __revision__ = '$Format:%H$' import os import math from qgis.PyQt.QtCore import Qt, QPointF, QRectF from qgis.PyQt.QtGui import QIcon, QFont, QFontMetricsF, QPen, QBrush, QColor, QPolygonF, QPicture, QPainter from qgis.PyQt.QtWidgets import QGraphicsItem, QMessageBox, QMenu from qgis.PyQt.QtSvg import QSvgRenderer from processing.modeler.ModelerAlgorithm import ModelerParameter, Algorithm, ModelerOutput from processing.modeler.ModelerParameterDefinitionDialog import ModelerParameterDefinitionDialog from processing.modeler.ModelerParametersDialog import ModelerParametersDialog pluginPath = os.path.split(os.path.dirname(__file__))[0] class ModelerGraphicItem(QGraphicsItem): BOX_HEIGHT = 30 BOX_WIDTH = 200 def __init__(self, element, model, controls): super(ModelerGraphicItem, self).__init__(None) self.controls = controls self.model = model self.element = element if isinstance(element, ModelerParameter): svg = QSvgRenderer(os.path.join(pluginPath, 'images', 'input.svg')) self.picture = QPicture() painter = QPainter(self.picture) svg.render(painter) self.pixmap = None self.text = element.param.description elif isinstance(element, ModelerOutput): # Output name svg = QSvgRenderer(os.path.join(pluginPath, 'images', 'output.svg')) self.picture = QPicture() painter = QPainter(self.picture) svg.render(painter) self.pixmap = None self.text = element.description else: self.text = element.description self.pixmap = element.algorithm.getIcon().pixmap(15, 15) self.arrows = [] self.setFlag(QGraphicsItem.ItemIsMovable, True) self.setFlag(QGraphicsItem.ItemIsSelectable, True) self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True) self.setZValue(1000) if not isinstance(element, ModelerOutput) and controls: svg = QSvgRenderer(os.path.join(pluginPath, 'images', 'edit.svg')) picture = QPicture() painter = QPainter(picture) svg.render(painter) pt = QPointF(ModelerGraphicItem.BOX_WIDTH / 2 - FlatButtonGraphicItem.WIDTH / 2, ModelerGraphicItem.BOX_HEIGHT / 2 - FlatButtonGraphicItem.HEIGHT / 2) self.editButton = FlatButtonGraphicItem(picture, pt, self.editElement) self.editButton.setParentItem(self) svg = QSvgRenderer(os.path.join(pluginPath, 'images', 'delete.svg')) picture = QPicture() painter = QPainter(picture) svg.render(painter) pt = QPointF(ModelerGraphicItem.BOX_WIDTH / 2 - FlatButtonGraphicItem.WIDTH / 2, - ModelerGraphicItem.BOX_HEIGHT / 2 + FlatButtonGraphicItem.HEIGHT / 2) self.deleteButton = FlatButtonGraphicItem(picture, pt, self.removeElement) self.deleteButton.setParentItem(self) if isinstance(element, Algorithm): alg = element.algorithm if alg.parameters: pt = self.getLinkPointForParameter(-1) pt = QPointF(0, pt.y()) if controls: self.inButton = FoldButtonGraphicItem(pt, self.foldInput, self.element.paramsFolded) self.inButton.setParentItem(self) if alg.outputs: pt = self.getLinkPointForOutput(-1) pt = QPointF(0, pt.y()) if controls: self.outButton = FoldButtonGraphicItem(pt, self.foldOutput, self.element.outputsFolded) self.outButton.setParentItem(self) def foldInput(self, folded): self.element.paramsFolded = folded self.prepareGeometryChange() if self.element.algorithm.outputs: pt = self.getLinkPointForOutput(-1) pt = QPointF(0, pt.y()) self.outButton.position = pt for arrow in self.arrows: arrow.updatePath() self.update() def foldOutput(self, folded): self.element.outputsFolded = folded self.prepareGeometryChange() for arrow in self.arrows: arrow.updatePath() self.update() def addArrow(self, arrow): self.arrows.append(arrow) def boundingRect(self): font = QFont('Verdana', 8) font.setPixelSize(12) fm = QFontMetricsF(font) unfolded = isinstance(self.element, Algorithm) and not self.element.paramsFolded numParams = len(self.element.algorithm.parameters) if unfolded else 0 unfolded = isinstance(self.element, Algorithm) and not self.element.outputsFolded numOutputs = len(self.element.algorithm.outputs) if unfolded else 0 hUp = fm.height() * 1.2 * (numParams + 2) hDown = fm.height() * 1.2 * (numOutputs + 2) rect = QRectF(-(ModelerGraphicItem.BOX_WIDTH + 2) / 2, -(ModelerGraphicItem.BOX_HEIGHT + 2) / 2 - hUp, ModelerGraphicItem.BOX_WIDTH + 2, ModelerGraphicItem.BOX_HEIGHT + hDown + hUp) return rect def mouseDoubleClickEvent(self, event): self.editElement() def contextMenuEvent(self, event): if isinstance(self.element, ModelerOutput): return popupmenu = QMenu() removeAction = popupmenu.addAction('Remove') removeAction.triggered.connect(self.removeElement) editAction = popupmenu.addAction('Edit') editAction.triggered.connect(self.editElement) if isinstance(self.element, Algorithm): if not self.element.active: removeAction = popupmenu.addAction('Activate') removeAction.triggered.connect(self.activateAlgorithm) else: deactivateAction = popupmenu.addAction('Deactivate') deactivateAction.triggered.connect(self.deactivateAlgorithm) popupmenu.exec_(event.screenPos()) def deactivateAlgorithm(self): self.model.deactivateAlgorithm(self.element.name) self.model.updateModelerView() def activateAlgorithm(self): if self.model.activateAlgorithm(self.element.name): self.model.updateModelerView() else: QMessageBox.warning(None, 'Could not activate Algorithm', 'The selected algorithm depends on other currently non-active algorithms.\n' 'Activate them them before trying to activate it.') def editElement(self): if isinstance(self.element, ModelerParameter): dlg = ModelerParameterDefinitionDialog(self.model, param=self.element.param) dlg.exec_() if dlg.param is not None: self.model.updateParameter(dlg.param) self.element.param = dlg.param self.text = dlg.param.description self.update() elif isinstance(self.element, Algorithm): dlg = self.element.algorithm.getCustomModelerParametersDialog(self.model, self.element.name) if not dlg: dlg = ModelerParametersDialog(self.element.algorithm, self.model, self.element.name) dlg.exec_() if dlg.alg is not None: dlg.alg.name = self.element.name self.model.updateAlgorithm(dlg.alg) self.model.updateModelerView() def removeElement(self): if isinstance(self.element, ModelerParameter): if not self.model.removeParameter(self.element.param.name): QMessageBox.warning(None, 'Could not remove element', 'Other elements depend on the selected one.\n' 'Remove them before trying to remove it.') else: self.model.updateModelerView() elif isinstance(self.element, Algorithm): if not self.model.removeAlgorithm(self.element.name): QMessageBox.warning(None, 'Could not remove element', 'Other elements depend on the selected one.\n' 'Remove them before trying to remove it.') else: self.model.updateModelerView() def getAdjustedText(self, text): font = QFont('Verdana', 8) font.setPixelSize(12) fm = QFontMetricsF(font) w = fm.width(text) if w < self.BOX_WIDTH - 25 - FlatButtonGraphicItem.WIDTH: return text text = text[0:-3] + '...' w = fm.width(text) while w > self.BOX_WIDTH - 25 - FlatButtonGraphicItem.WIDTH: text = text[0:-4] + '...' w = fm.width(text) return text def paint(self, painter, option, widget=None): rect = QRectF(-(ModelerGraphicItem.BOX_WIDTH + 2) / 2.0, -(ModelerGraphicItem.BOX_HEIGHT + 2) / 2.0, ModelerGraphicItem.BOX_WIDTH + 2, ModelerGraphicItem.BOX_HEIGHT + 2) if isinstance(self.element, ModelerParameter): color = QColor(238, 242, 131) outline = QColor(234, 226, 118) selected = QColor(116, 113, 68) elif isinstance(self.element, Algorithm): color = QColor(255, 255, 255) outline = Qt.gray selected = QColor(50, 50, 50) else: color = QColor(172, 196, 114) outline = QColor(90, 140, 90) selected = QColor(42, 65, 42) if self.isSelected(): outline = selected color = color.darker(110) painter.setPen(QPen(outline, 0)) # 0 width "cosmetic" pen painter.setBrush(QBrush(color, Qt.SolidPattern)) painter.drawRect(rect) font = QFont('Verdana', 8) font.setPixelSize(12) painter.setFont(font) painter.setPen(QPen(Qt.black)) text = self.getAdjustedText(self.text) if isinstance(self.element, Algorithm) and not self.element.active: painter.setPen(QPen(Qt.gray)) text = text + "\n(deactivated)" fm = QFontMetricsF(font) text = self.getAdjustedText(self.text) h = fm.ascent() pt = QPointF(-ModelerGraphicItem.BOX_WIDTH / 2 + 25, ModelerGraphicItem.BOX_HEIGHT / 2.0 - h + 1) painter.drawText(pt, text) painter.setPen(QPen(Qt.black)) if isinstance(self.element, Algorithm): h = -(fm.height() * 1.2) h = h - ModelerGraphicItem.BOX_HEIGHT / 2.0 + 5 pt = QPointF(-ModelerGraphicItem.BOX_WIDTH / 2 + 25, h) painter.drawText(pt, 'In') i = 1 if not self.element.paramsFolded: for param in self.element.algorithm.parameters: if not param.hidden: text = self.getAdjustedText(param.description) h = -(fm.height() * 1.2) * (i + 1) h = h - ModelerGraphicItem.BOX_HEIGHT / 2.0 + 5 pt = QPointF(-ModelerGraphicItem.BOX_WIDTH / 2 + 33, h) painter.drawText(pt, text) i += 1 h = fm.height() * 1.1 h = h + ModelerGraphicItem.BOX_HEIGHT / 2.0 pt = QPointF(-ModelerGraphicItem.BOX_WIDTH / 2 + 25, h) painter.drawText(pt, 'Out') if not self.element.outputsFolded: for i, out in enumerate(self.element.algorithm.outputs): text = self.getAdjustedText(out.description) h = fm.height() * 1.2 * (i + 2) h = h + ModelerGraphicItem.BOX_HEIGHT / 2.0 pt = QPointF(-ModelerGraphicItem.BOX_WIDTH / 2 + 33, h) painter.drawText(pt, text) if self.pixmap: painter.drawPixmap(-(ModelerGraphicItem.BOX_WIDTH / 2.0) + 3, -8, self.pixmap) elif self.picture: painter.drawPicture(-(ModelerGraphicItem.BOX_WIDTH / 2.0) + 3, -8, self.picture) def getLinkPointForParameter(self, paramIndex): offsetX = 25 if isinstance(self.element, Algorithm) and self.element.paramsFolded: paramIndex = -1 offsetX = 17 font = QFont('Verdana', 8) font.setPixelSize(12) fm = QFontMetricsF(font) if isinstance(self.element, Algorithm): h = -(fm.height() * 1.2) * (paramIndex + 2) - fm.height() / 2.0 + 8 h = h - ModelerGraphicItem.BOX_HEIGHT / 2.0 else: h = 0 return QPointF(-ModelerGraphicItem.BOX_WIDTH / 2 + offsetX, h) def getLinkPointForOutput(self, outputIndex): if isinstance(self.element, Algorithm) and self.element.algorithm.outputs: outputIndex = (outputIndex if not self.element.outputsFolded else -1) text = self.getAdjustedText(self.element.algorithm.outputs[outputIndex].description) font = QFont('Verdana', 8) font.setPixelSize(12) fm = QFontMetricsF(font) w = fm.width(text) h = fm.height() * 1.2 * (outputIndex + 1) + fm.height() / 2.0 y = h + ModelerGraphicItem.BOX_HEIGHT / 2.0 + 5 x = (-ModelerGraphicItem.BOX_WIDTH / 2 + 33 + w + 5 if not self.element.outputsFolded else 10) return QPointF(x, y) else: return QPointF(0, 0) def itemChange(self, change, value): if change == QGraphicsItem.ItemPositionHasChanged: for arrow in self.arrows: arrow.updatePath() self.element.pos = self.pos() return value def polygon(self): font = QFont('Verdana', 8) font.setPixelSize(12) fm = QFontMetricsF(font) hUp = fm.height() * 1.2 * (len(self.element.parameters) + 2) hDown = fm.height() * 1.2 * (len(self.element.outputs) + 2) pol = QPolygonF([ QPointF(-(ModelerGraphicItem.BOX_WIDTH + 2) / 2, -(ModelerGraphicItem.BOX_HEIGHT + 2) / 2 - hUp), QPointF(-(ModelerGraphicItem.BOX_WIDTH + 2) / 2, (ModelerGraphicItem.BOX_HEIGHT + 2) / 2 + hDown), QPointF((ModelerGraphicItem.BOX_WIDTH + 2) / 2, (ModelerGraphicItem.BOX_HEIGHT + 2) / 2 + hDown), QPointF((ModelerGraphicItem.BOX_WIDTH + 2) / 2, -(ModelerGraphicItem.BOX_HEIGHT + 2) / 2 - hUp), QPointF(-(ModelerGraphicItem.BOX_WIDTH + 2) / 2, -(ModelerGraphicItem.BOX_HEIGHT + 2) / 2 - hUp) ]) return pol class FlatButtonGraphicItem(QGraphicsItem): WIDTH = 16 HEIGHT = 16 def __init__(self, picture, position, action): super(FlatButtonGraphicItem, self).__init__(None) self.setAcceptHoverEvents(True) self.setFlag(QGraphicsItem.ItemIsMovable, False) self.picture = picture self.position = position self.isIn = False self.action = action def mousePressEvent(self, event): self.action() def paint(self, painter, option, widget=None): pt = QPointF(-math.floor(self.WIDTH / 2), -math.floor(self.HEIGHT / 2)) + self.position rect = QRectF(pt.x(), pt.y(), self.WIDTH, self.HEIGHT) if self.isIn: painter.setPen(QPen(Qt.transparent, 1)) painter.setBrush(QBrush(QColor(55, 55, 55, 33), Qt.SolidPattern)) else: painter.setPen(QPen(Qt.transparent, 1)) painter.setBrush(QBrush(Qt.transparent, Qt.SolidPattern)) painter.drawRect(rect) painter.drawPicture(pt.x(), pt.y(), self.picture) def boundingRect(self): rect = QRectF(self.position.x() - math.floor(self.WIDTH / 2), self.position.y() - math.floor(self.HEIGHT / 2), self.WIDTH, self.HEIGHT) return rect def hoverEnterEvent(self, event): self.isIn = True self.update() def hoverLeaveEvent(self, event): self.isIn = False self.update() class FoldButtonGraphicItem(FlatButtonGraphicItem): WIDTH = 11 HEIGHT = 11 def __init__(self, position, action, folded): plus = QPicture() minus = QPicture() svg = QSvgRenderer(os.path.join(pluginPath, 'images', 'plus.svg')) painter = QPainter(plus) svg.render(painter) svg = QSvgRenderer(os.path.join(pluginPath, 'images', 'minus.svg')) painter = QPainter(minus) svg.render(painter) self.pictures = {True: plus, False: minus} self.folded = folded picture = self.pictures[self.folded] super(FoldButtonGraphicItem, self).__init__(picture, position, action) def mousePressEvent(self, event): self.folded = not self.folded self.picture = self.pictures[self.folded] self.action(self.folded)