def createNetPath(self, brushColor: str, painterPath: QPainterPath, isOccupyPathItem: bool): """ Create a QGraphicsPathItem for a network path Args: brushColor (str) : The color for filling the rectangles painterPath (QPainterPath) : The path to be inserted to the item isOccupyPathItem (bool) : Whether the path is occupied or unoccupied path """ # Generate the path item if not created if isOccupyPathItem: if self.occupiedPathItem is None: self.occupiedPathItem = QGraphicsPathItem(self) pathItem = self.occupiedPathItem else: if self.unoccupiedPathItem is None: self.unoccupiedPathItem = QGraphicsPathItem(self) pathItem = self.unoccupiedPathItem if pathItem is None: pathItem = QGraphicsPathItem(self) # Set the item parameters pathItem.setPath(painterPath) pathItem.setPen( QPen(QColor(self.netColor), self.netThickness, style=Qt.SolidLine)) pathItem.setBrush(QBrush(QColor(brushColor))) pathItem.setZValue(0)
class createDiscretionWindow(view.descritiondefinition_ui.Ui_discretionWindow, QDialog): def __init__(self, isCrack, he, parent=None): super(createDiscretionWindow, self).__init__(parent) self.highlight = QGraphicsPathItem() self.highlight.setPath(he) pen = QPen(Qt.red, 2) self.highlight.setPen(pen) self.highlight.setZValue(1) self.parent().scene.addItem(self.highlight) self.setupUi(self) self.move(1450, -1) self.isCrack = isCrack self.lineEdit_number_of_elements.setFocus(True) self.lineEdit_number_of_elements.textChanged.connect(self.change_number_of_discontinous_elements) self.radioButton_2.toggled.connect(self.setDiscontinousBoxState) if self.isCrack == True: self.checkBox_isCrack.setChecked(True) self.radioButton_2.setChecked(True) self.checkBox_isCrack.setEnabled(False) self.groupBox_type_of_discretization.setEnabled(False) def setDiscontinousBoxState(self): newState = not self.groupBox_discontinous_discretion.isEnabled() self.groupBox_discontinous_discretion.setEnabled(newState) def change_number_of_discontinous_elements(self): if self.radioButton_2.isChecked(): if self.lineEdit_number_of_elements.text() != "": if int(self.lineEdit_number_of_elements.text()) > self.verticalLayout_2.count(): for i in range(int(self.lineEdit_number_of_elements.text()) - self.verticalLayout_2.count()): lineEdit = QLineEdit() validator = QDoubleValidator() validator.setNotation(0) validator.setRange(0.1, 0.9, decimals=3) lineEdit.setValidator(validator) self.verticalLayout_2.addWidget(lineEdit) else: for i in range(self.verticalLayout_2.count()-1, int(self.lineEdit_number_of_elements.text())-1, -1): itemToDelete = self.verticalLayout_2.itemAt(i).widget() self.verticalLayout_2.removeWidget(itemToDelete) itemToDelete.deleteLater() else: pass @classmethod def getDiscretion(cls, he, isCrack=False, parent=None): dialog = cls(isCrack, he, parent) dialog.exec_() if dialog.radioButton.isChecked(): dialog.parent().scene.removeItem(dialog.highlight) if dialog.lineEdit_number_of_elements.text() != "": return int(dialog.lineEdit_number_of_elements.text()), False else: return 1, False else: spaces = [0] for i in range(dialog.verticalLayout_2.count()): spaces.append(float(dialog.verticalLayout_2.itemAt(i).widget().text()) + spaces[-1]) dialog.parent().scene.removeItem(dialog.highlight) return spaces, True
def createNetPath(self, brushColor: str, painterPath: QPainterPath, isOccupyPathItem: bool): """ Create a QGraphicsPathItem for a network path Args: brushColor (str) : The color for filling the rectangles painterPath (QPainterPath) : The path to be inserted to the item isOccupyPathItem (bool) : Whether the path is occupied or unoccupied path """ # Generate the path item if not created if isOccupyPathItem: if self.occupiedPathItem is None: self.occupiedPathItem = QGraphicsPathItem(self) pathItem = self.occupiedPathItem else: if self.unoccupiedPathItem is None: self.unoccupiedPathItem = QGraphicsPathItem(self) pathItem = self.unoccupiedPathItem if pathItem is None: pathItem = QGraphicsPathItem(self) # Set the item parameters pathItem.setPath(painterPath) pathItem.setPen(QPen(QColor(self.netColor), self.netThickness, style=Qt.SolidLine)) pathItem.setBrush(QBrush(QColor(brushColor))) pathItem.setZValue(0)
def updateConnector(self, item: QGraphicsPathItem, p1: QPointF, p2: QPointF, select: bool = True): path = QPainterPath(p1) path.quadTo(p1 + QPointF(-15, 0), (p1 + p2) * 0.5) path.quadTo(p2 + QPointF(15, 0), p2) if item is None: item = self.nodesScene.addPath(path) else: item.setPath(path) item.setZValue(-1) if select: item.setFlag(QGraphicsItem.ItemIsSelectable) return item
def FillScene(aScene, aProcessGraphics): maxRow = 0 maxCol = 0 for graphic in aProcessGraphics: maxRow = max(maxRow, graphic.row) maxCol = max(maxCol, graphic.col) maxRow = maxRow + 1 maxCol = maxCol + 1 colWidth = 400 rowHeigth = 360 border = 50 sceneWidth = colWidth * maxCol + border * 2 sceneHeight = rowHeigth * maxRow + border * 2 aScene.setSceneRect(QRectF(0, 0, sceneWidth, sceneHeight)) aScene.clear() ## Set process positions for graphic in aProcessGraphics: x = 50 + sceneWidth - (graphic.col + 1) * colWidth y = 50 + graphic.row * rowHeigth graphic.setPos(QPointF(x, y)) aScene.addItem(graphic) ## Add lines for graphic in aProcessGraphics: for inp in graphic.inputs: for child in inp.children: controlOffset = 100 start = child.GetScenePos() control1 = start + QPointF(controlOffset, 0) end = inp.GetScenePos() control2 = end + QPointF(-controlOffset, 0) path = QPainterPath() path.moveTo(start) path.cubicTo(control1, control2, end) line = QGraphicsPathItem(path) line.setZValue(-1000) aScene.addItem(line)
class PathMaker(QWidget): def __init__(self, parent): super().__init__() self.canvas = parent self.scene = parent.scene self.view = parent.view self.dots = parent.dots self.chooser = None ## placeholder for popup_widget self.initThis() self.sliders = self.dots.sliderpanel ## for toggling key menu self.sideWays = SideWays(self) ## extends pathMaker self.direct = { 'F': self.sideWays.openFiles, 'C': self.sideWays.centerPath, 'P': self.pathChooser, '{': self.sideWays.flipPath, '}': self.sideWays.flopPath, } self.setMouseTracking(True) self.view.viewport().installEventFilter(self) ### -------------------------------------------------------- def initThis(self): self.pts = [] self.npts = 0 ## counter used by addNewPathPts self.key = "" self.color = "DODGERBLUE" self.openPathFile = '' self.pointTag = "" self.tag = '' self.pathSet = False self.newPathSet = False self.pathChooserSet = False self.ball = None self.path = None self.newPath = None self.pathBall = None self.tagGroup = None self.pathTestSet = False self.wayPtsSet = False ## appear as tags ### ---------------------- key handler --------------------- @pyqtSlot(str) def pathKeys(self, key): self.key = key if self.key == 'D': ## always self.delete() elif self.key == '/': self.sideWays.changePathColor() elif self.newPathSet and self.key == 'cmd': ## note self.closeNewPath() elif key in NotPathSetKeys: if self.key == 'R': self.sideWays.reversePath() elif self.key == 'S': self.sideWays.savePath() elif self.key == 'T': self.sideWays.pathTest() elif self.key == 'W': self.sideWays.addWayPtTags() elif self.key == 'N': if not self.pathSet and not self.wayPtsSet: if not self.newPathSet: self.addNewPath() else: self.newPathOff() ## changed your mind self.delete() ## not waypts and not new path elif not self.wayPtsSet and not self.newPathSet: if key in self.direct: self.direct[key]() ## OK.. elif key in MoveKeys: self.sideWays.movePath(key) elif key in ScaleRotateKeys: self.sideWays.scaleRotate(key) ## waypts only elif self.wayPtsSet and key in WayPtsKeys: if self.key == '!': self.sideWays.halfPath() elif self.key == 'V': self.togglePointItems() elif self.key in ('<', '>'): self.sideWays.shiftWayPts(key) ### ----------------- event filter..not used --------------- ''' PathMaker mouse events for drawing a path ''' def eventFilter(self, source, e): if self.canvas.pathMakerOn and self.newPathSet: if e.type() == QEvent.MouseButtonPress: self.npts = 0 self.addNewPathPts(QPoint(e.pos())) elif e.type() == QEvent.MouseMove: self.addNewPathPts(QPoint(e.pos())) elif e.type() == QEvent.MouseButtonRelease: self.addNewPathPts(QPoint(e.pos())) self.updateNewPath() return False return QWidget.eventFilter(self, source, e) ### -------------------------------------------------------- def initPathMaker(self): ## from docks button if self.scene.items() and not self.canvas.pathMakerOn: MsgBox("Clear Scene First to run PathMaker") return elif self.canvas.pathMakerOn: self.pathMakerOff() else: self.canvas.pathMakerOn = True self.scene.clear() self.initThis() if not self.sliders.pathMenuSet: self.sliders.toggleMenu() self.turnGreen() # QTimer.singleShot(200, self.pathChooser) ## optional def turnGreen(self): self.dots.btnPathMaker.setStyleSheet("background-color: LIGHTGREEN") def delete(self): self.stopPathTest() self.removePointItems() self.removePath() self.removeWayPtTags() self.removeNewPath() self.pathChooserOff() self.scene.clear() self.initThis() def pathMakerOff(self): self.delete() self.canvas.pathMakerOn = False if self.sliders.pathMenuSet: self.sliders.toggleMenu() self.dots.btnPathMaker.setStyleSheet("background-color: white") def pathChooser(self): if not self.pathChooserSet and not self.newPathSet: self.chooser = DoodleMaker(self) self.chooser.move(600, 200) self.chooser.show() self.pathChooserSet = True else: self.pathChooserOff() def pathChooserOff(self): self.chooser = None self.pathChooserSet = False ### -------------------- new path ------------------------- def addNewPathPts(self, pt): ## called by dropCanvas eventfilter if self.npts == 0: self.pts.append(pt) self.npts += 1 if self.npts % 3 == 0: self.pts.append(pt) self.updateNewPath() def addNewPath(self): self.dots.btnPathMaker.setStyleSheet( "background-color: rgb(215,165,255)") self.initNewPath(True) def initNewPath(self, bool): self.newPath = None self.newPathSet = bool self.pts = [] self.npts = 0 def closeNewPath(self): ## applies only to newPath self.removeNewPath() self.addPath() ## add the completed path self.turnGreen() def newPathOff(self): if self.newPathSet: if self.newPath: self.scene.removeItem(self.newPath) self.initNewPath(False) self.turnGreen() def updateNewPath(self): if self.pts: ## list of points self.removeNewPath() ## clean up just in case self.newPath = QGraphicsPathItem(self.sideWays.setPaintPath()) self.newPath.setPen(QPen(QColor(self.color), 3, Qt.DashDotLine)) self.newPath.setZValue(common['pathZ']) self.scene.addItem(self.newPath) ## only one - no group needed self.newPathSet = True def removeNewPath(self): ## keep self.pts if self.newPath: self.scene.removeItem(self.newPath) self.newPathSet = False self.newPath = None ### -------------------- path stuff ------------------------ def addPath(self): self.removePath() self.path = QGraphicsPathItem(self.sideWays.setPaintPath(True)) self.path.setPen(QPen(QColor(self.color), 3, Qt.DashDotLine)) self.path.setZValue(common['pathZ']) self.scene.addItem(self.path) self.pathSet = True def removePath(self): if self.path: self.scene.removeItem(self.path) self.pathSet = False self.path = None ### --------------- pointItems and tags -------------------- def togglePointItems(self): if self.pointItemsSet(): self.removePointItems() else: self.addPointItems() QTimer.singleShot(200, self.redrawPathsAndTags) def redrawPathsAndTags(self): self.removeWayPtTags() self.removePath() self.addPath() self.sideWays.addWayPtTags() def findTop(self): for itm in self.scene.items(): return itm.zValue() return 0 def printZ(self): ## alternate print(self.scene.items()[0].zValue()) def addPointItems(self): idx = 0 add = self.findTop() + 10 for pt in self.pts: self.scene.addItem(PointItem(self, pt, idx, add)) idx += 1 def removePointItems(self): for pt in self.scene.items(): if pt.type == 'pt': self.scene.removeItem(pt) def pointItemsSet(self): for itm in self.scene.items(): if itm.type == 'pt': return True return False def insertPointItem(self, pointItem): idx, pt = pointItem.idx + 1, pointItem.pt if idx == len(self.pts): idx = 0 pt1 = self.pts[idx] pt1 = QPointF(pt1.x() - pt.x(), pt1.y() - pt.y()) pt1 = pt + QPointF(pt1) * .5 self.pts.insert(idx, pt1) self.redrawPoints() def delPointItem(self, pointItem): self.pts.pop(pointItem.idx) self.redrawPoints() def redrawPoints(self, bool=True): ## pointItems points self.removePointItems() self.removeWayPtTags() self.removePath() self.addPath() self.sideWays.addWayPtTags() if bool: self.addPointItems() def addPointTag(self, pnt): ## single tag pct = (pnt.idx / len(self.pts)) * 100 tag = self.sideWays.makePtsTag(pnt.pt, pnt.idx, pct) self.pointTag = TagIt('points', tag, QColor("YELLOW")) p = QPointF(0, -20) self.pointTag.setPos(pnt.pt + p) self.pointTag.setZValue(self.findTop() + 5) self.scene.addItem(self.pointTag) def addWayPtTag(self, tag, pt): self.tag = TagIt('pathMaker', tag, QColor("TOMATO")) self.tag.setPos(pt) self.tag.setZValue(common["tagZ"] + 5) self.tagGroup.addToGroup(self.tag) def removePointTag(self): ## single tag if self.pointTag: self.scene.removeItem(self.pointTag) self.pointTag = '' def addWayPtTagsGroup(self): self.tagGroup = QGraphicsItemGroup() self.scene.addItem(self.tagGroup) self.tagGroup.setZValue(common["tagZ"] + 5) def removeWayPtTags(self): if self.tagGroup or self.wayPtsSet: self.scene.removeItem(self.tagGroup) self.tagGroup = None self.wayPtsSet = False def startPathTest(self): self.scene.addItem(self.ball) self.pathBall.start() self.pathTestSet = True def stopPathTest(self): if self.pathTestSet: self.pathBall.stop() self.scene.removeItem(self.ball) self.pathBall = None self.pathTestSet = False
class CharItem(QGraphicsRectItem): """ This item represents character item The purpose of the class is to draw a character, create a matrix of rectangles and resolve in which rectangles the character passes The class allow the following operations -# Drawing a character using the mouse events: -# Start by the mouse press event -# Continues by the mouse move event -# The character is stored in QGraphicsPathItem -# Transform the character to occupy the whole item's space -# Set operation : resolving the Occupied matrix which tell on which rectangle the character passes -# Reset operation : reverse the character transform so it is possible to continue drawing the character -# Save operation : To a QDataStream -# Load operation : From a QDataStream The graphical view of the class is composed from: -# This class which inherits from QGraphicsRectItem and holds : -# A QGraphicsPathItem : representing the character -# A QGraphicsPathItem : representing the occupied rectangles -# A QGraphicsPathItem : representing the unoccupied rectangles """ def __init__(self, rect: QRectF, pos: QPointF, viewIndex: int = -1): """ CharItem constructor Args: rect (QRectF) : The rectangle that the character should fill pos (QPointF) : The position of the item within the parent viewIndex (int) : The index of the item in case it is presented in multi character presentation """ super(CharItem, self).__init__(rect) self.setAcceptedMouseButtons(Qt.LeftButton) self.setPresentationPrms() self.occupied = [[False for idx in range(self.netCols)] for idx in range(self.netRows)] self.charPath = None self.wasSetted = False self.occupiedPathItem = None self.unoccupiedPathItem = None self.dirty = False self.viewIndex = viewIndex self.filename = "" self.boundaries = rect self.dx = 1 self.dy = 1 self.posInParent = pos self.setPos(self.posInParent) def setPresentationPrms(self): """ Setting the presentation prms The reason the there is a duplicate set of presentation parameters is that it allows changing the presentation parameters for one character (like in the select option """ self.netColor = netColor self.netThickness = netThickness self.occupyColor = occupyColor self.unOccupyColor = unOccupyColor self.shapeColor = shapeColor self.shapeLineThickness = shapeLineThickness self.selectedOccupiedColor = selectedOccupiedColor self.selectedShapeColor = selectedShapeColor self.netRows = netRows self.netCols = netCols def setNetBoxDimensions(self, rect: QRectF): """ Set net box dimensions The net box is the rectangle that compose the network drawn to show the occupy matrix """ self.netRectHeight = rect.height() / self.netRows self.netRectWidth = rect.width() / self.netCols self.left = rect.left() self.top = rect.top() def netRect(self, row_idx: int, col_idx: int) -> QRectF: """ Set net rect The net box is the rectangle that compose the network drawn to show the occupy matrix Args: row_idx (int) : The row of the network rectangle col_idx (int) : The col of the network rectangle Returns: QRectF : The rectangle """ return QRectF(self.left + col_idx * self.netRectWidth, self.top + row_idx * self.netRectHeight, self.netRectWidth, self.netRectHeight) def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent): """ Mouse move event : continue draw a line This event is activated when the mouse is pressed and moves The methods draws the line in 2 conditions: -# The item is not part of multi character presentation -# A character path was initiated (using the mouse press event) Args: event (QGraphicsSceneMouseEvent) : the event description """ if self.viewIndex == -1: if self.charPath is not None: point = event.scenePos() path = self.charPath.path() path.lineTo(point) self.charPath.setPath(path) self.update() def mousePressEvent(self, event: QGraphicsSceneMouseEvent): """ Mouse Press Event : Start a new line / Select the character If the character is part of multi character presentation activate the character selection If the character is in single character presentation Start a new line in the character Args: event (QGraphicsSceneMouseEvent) : the event description """ if self.viewIndex == -1: self.startLine(event) else: self.setSelected() def startLine(self, event: QGraphicsSceneMouseEvent): """ Start drawing a line When the mouse button is pressed and we are in single character dialog this method is activated to start drowning a line in the character Args: event (QGraphicsSceneMouseEvent) : the event description """ # There are 2 modes for the presentation: # Original mode where the character is it's original size # After setting mode when the set was done and the character fullfill all # the item's space # Drawing can be done only in original mode if self.wasSetted: QMessageBox.critical( None, "Char identifier window", "The shape was already setted use revert setting") return # If this is the first start of a line - generate the QPainterPath and QGraphicsPathItem if self.charPath is None: self.initCharPath() # Move to the mouse position point = event.scenePos() path = self.charPath.path() path.moveTo(point) self.charPath.setPath(path) self.dirty = True def initCharPath(self): """ Init the item that holds the character There is one path item that holds the character This method is activated by start line if the char item was not created to create the new and only one """ self.dirty = True self.charPath = QGraphicsPathItem(self) self.charPath.setPen( QPen(QColor(self.shapeColor), self.shapeLineThickness)) self.charPath.setZValue(1) self.charPath.originalPos = self.charPath.pos() self.charPath.setPath(QPainterPath()) def setSelected(self): """ Set the item a selected item This method is activated when the mouse button is presses and the item is part of multi character presentation """ # Set the colors of the item self.occupiedPathItem.setBrush( QBrush(QColor(self.selectedOccupiedColor))) self.charPath.setPen( QPen(QColor(self.selectedShapeColor), self.shapeLineThickness)) self.update() # Report to the parent item about the selection self.parentItem().setSelected(self.viewIndex) def resetSelected(self): """ Set the colors of the item to not selected """ self.occupiedPathItem.setBrush(QBrush(QColor(self.occupyColor))) self.charPath.setPen( QPen(QColor(self.shapeColor), self.shapeLineThickness)) self.update() def set(self): """ Calculate the occupied matrix and present the results This method does the following: -# Fill the occupied matrix -# Generate the occupied and unoccupied pathes items -# Transform the char path to fit to the item's boundaries """ # If there is no shape drawn - return if self.charPath is None: QMessageBox.critical(None, "Char identifier window", "There is no shape drawn") return # If the item is in setted mode - return if self.wasSetted: QMessageBox.critical( None, "Char identifier window", "The shape was already setted use revert setting") return # fill the occupied matrix with the data before the scaling self.fillOccupied() self.setNetBoxDimensions(self.boundingRect()) self.createNetPaths() # update the transform - change the dimensions and location # only on the first time self.transformCharPath() self.wasSetted = True # update the presentation self.update() def revertTransform(self): """ Change from Setted mode to drawing mode The drawing mode is the mode where the character can be drawn -# Restore the original size of the character (Reset the transform of the char item) -# Restor the char path item's position to the original one (saved when created) -# Empty the occupiedPath and the unoccupiedPath """ # If there is no character drawn - return if self.charPath is None: QMessageBox.critical(None, "Char identifier window", "There is no shape drawn") return # If the item is already in drawing mode - return if not self.wasSetted: QMessageBox.critical(None, "Char identifier window", "The shape was not setted use set button") return # The char path item transform = self.charPath.transform() transform.reset() # The self.dx and self.dy are the scale parameters created when the item # begins and they are the scale parameters that transform it to the boundaries # given by the parent item transform.scale(self.dx, self.dy) self.charPath.setTransform(transform) self.charPath.setPos(self.charPath.originalPos) # Empty the network pathes self.occupiedPathItem.setPath(QPainterPath()) self.unoccupiedPathItem.setPath(QPainterPath()) self.wasSetted = False def transformCharPath(self): """ Transform char path when the item is setted This method does the following -# scale the char path to the size of the item -# calculate the new position of the char path so that it will be placed at the top left corner of the item """ dx = self.boundingRect().width() / self.charPath.boundingRect().width() dy = self.boundingRect().height() / self.charPath.boundingRect( ).height() transform = self.charPath.transform() transform.reset() transform.scale(dx, dy) self.charPath.setTransform(transform) # Move the shape to the origin moveX = -(self.charPath.boundingRect().left() - self.boundingRect().left()) * dx moveY = -(self.charPath.boundingRect().top() - self.boundingRect().top()) * dy self.charPath.setX(self.charPath.x() + moveX) self.charPath.setY(self.charPath.y() + moveY) def fillOccupied(self): """ Fill the occupied matrix The algorithm of filling the occupied matrix is -# Scanning the char path -# For each point decide on where row and column of the net -# Set the occupies matrix for this column and row to True """ for idx in range(100): point = self.charPath.path().pointAtPercent(idx / 100.) row_idx, col_idx = self.calcRowCol(point) self.occupied[row_idx][col_idx] = True def calcRowCol(self, point: QPointF): """ Calculate the network row and column that a point is int calc the row and column indexes of a point The following is the algorithm: 1. Find the distance between the point and the left (or top) 2. Divide the distance with the width of path to find the relative position 3. Multipile this relative position with the number of rows/cols 4. Convert the result to int to find the indexes 5. If the index is the number of row/col reduce the index (This is for the case the the point is on the boundary and in this case the relative position is 1 which will cause the indexes to be the number of rows/cols - out of the matrix indexes) Args: point (QPointF) : The point to resolve Returns: int : The network row that the point is in int : The network column that the point is in """ partialX = (point.x() - self.charPath.boundingRect().left() ) / self.charPath.boundingRect().width() partialY = (point.y() - self.charPath.boundingRect().top() ) / self.charPath.boundingRect().height() col_idx = int(partialX * self.netCols) row_idx = int(partialY * self.netRows) if row_idx == self.netRows: row_idx -= 1 if col_idx == self.netCols: col_idx -= 1 return row_idx, col_idx def createNetPaths(self): """ Create the network pathes This method creates 2 network pathes items one for holding the occupied rectangles and one to hold the unoccupied rectangles """ # Generate 2 QPainterPath occupiedPath = QPainterPath() unoccupiedPath = QPainterPath() # For each entry in occupied matrix : # Add a rectangle to the appropriate path according the entry value for row_idx in range(self.netRows): for col_idx in range(self.netCols): if self.occupied[row_idx][col_idx]: occupiedPath.addRect(self.netRect(row_idx, col_idx)) else: unoccupiedPath.addRect(self.netRect(row_idx, col_idx)) # Create the QGraphicsPathItems that will hold the path self.createNetPath(self.occupyColor, occupiedPath, True) self.createNetPath(self.unOccupyColor, unoccupiedPath, False) def createNetPath(self, brushColor: str, painterPath: QPainterPath, isOccupyPathItem: bool): """ Create a QGraphicsPathItem for a network path Args: brushColor (str) : The color for filling the rectangles painterPath (QPainterPath) : The path to be inserted to the item isOccupyPathItem (bool) : Whether the path is occupied or unoccupied path """ # Generate the path item if not created if isOccupyPathItem: if self.occupiedPathItem is None: self.occupiedPathItem = QGraphicsPathItem(self) pathItem = self.occupiedPathItem else: if self.unoccupiedPathItem is None: self.unoccupiedPathItem = QGraphicsPathItem(self) pathItem = self.unoccupiedPathItem if pathItem is None: pathItem = QGraphicsPathItem(self) # Set the item parameters pathItem.setPath(painterPath) pathItem.setPen( QPen(QColor(self.netColor), self.netThickness, style=Qt.SolidLine)) pathItem.setBrush(QBrush(QColor(brushColor))) pathItem.setZValue(0) def save(self, stream: QDataStream, filename: str): """ Save the item to QDataStream Args: stream (QDataStream) : The data stream to write the item to filename (str) : The filename (for documenting purposes) """ # The item position stream << self.pos() # The dimensions stream << self.rect() # The presentation parameters stream.writeQString(self.netColor) stream.writeQString(self.occupyColor) stream.writeQString(self.unOccupyColor) stream.writeQString(self.shapeColor) stream.writeInt16(self.shapeLineThickness) stream.writeInt16(self.netRows) stream.writeInt16(self.netRows) # The items paths stream << self.charPath.path() self.dirty = False self.filename = filename def load(self, stream, filename): """ Loads the item from QDataStream Args: stream (QDataStream) : The data stream to read the item from filename (str) : The filename (for documenting purposes) """ # read the pos pos = QPointF() stream >> pos self.setPos(pos) # read the dimensions rect = QRectF() stream >> rect self.setRect(rect) # The presentation parameters self.netColor = stream.readQString() self.occupyColor = stream.readQString() self.unOccupyColor = stream.readQString() self.shapeColor = stream.readQString() self.shapeLineThickness = stream.readInt16() self.netRows = stream.readInt16() self.netRows = stream.readInt16() # read the paths self.initCharPath() path = self.charPath.path() stream >> path self.charPath.setPath(path) # Fit the item to the boundaries and position given by the item's parent self.fitToBoundaries() # The presentation of the item is in setted mode so we activate the set method self.wasSetted = False self.set() self.dirty = False self.filename = filename def fitToBoundaries(self): """ Fit the item to the boundaries and position given by it's parent This method was made to support the change of the character boundaries and that the char can be presented in different boundaries and position """ self.setPos(self.posInParent) self.dx = self.boundaries.width() / self.rect().width() self.dy = self.boundaries.height() / self.rect().height() transform = self.transform() transform.scale(self.dx, self.dy) self.setTransform(transform)
class MapData(dict): def __init__(self, zone=None): super().__init__() self.zone = zone self.raw = {'lines': [], 'poi': [], 'grid': []} self.geometry = None # MapGeometry self.players = {} self.spawns = [] self.way_point = None self.grid = None if self.zone is not None: self._load() def _load(self): # Get list of all map files for current zone map_file_name = MapData.get_zone_dict()[self.zone.strip().lower()] extensions = ['.txt', '_1.txt', '_2.txt', '_3.txt', '_4.txt', '_5.txt'] maps = [ os.path.join(MAP_FILES_LOCATION, m) for m in [(map_file_name + e) for e in extensions] if os.path.exists(os.path.join(MAP_FILES_LOCATION, m)) ] all_x, all_y, all_z = [], [], [] # TODO: Remove the references to raw # Create Lines and Points for map_file in maps: with open(map_file, 'r') as f: for line in f.readlines(): line_type = line.lower()[0:1] data = [value.strip() for value in line[1:].split(',')] if line_type == 'l': # line x1, y1, z1, x2, y2, z2 = list(map(float, data[0:6])) self.raw['lines'].append( MapLine(x1=x1, y1=y1, z1=z1, x2=x2, y2=y2, z2=z2, color=self.color_transform( QColor(int(data[6]), int(data[7]), int(data[8]))))) all_x.extend((x1, x2)) all_y.extend((y1, y2)) all_z.append(min(z1, z2)) # if abs(z1 - z2) < 2: # if z1 == z2: # all_z.extend((z1, z2)) elif line_type == 'p': # point x, y, z = map(float, data[0:3]) self.raw['poi'].append( MapPoint(x=x, y=y, z=z, size=int(data[6]), text=str(data[7]), color=self.color_transform( QColor(int(data[3]), int(data[4]), int(data[5]))))) # Create Grid Lines lowest_x, highest_x, lowest_y, highest_y, lowest_z, highest_z = min( all_x), max(all_x), min(all_y), max(all_y), min(all_z), max(all_z) left, right = int(math.floor(lowest_x / 1000) * 1000), int( math.ceil(highest_x / 1000) * 1000) top, bottom = int(math.floor(lowest_y / 1000) * 1000), int( math.ceil(highest_y / 1000) * 1000) for number in range(left, right + 1000, 1000): self.raw['grid'].append( MapLine(x1=number, x2=number, y1=top, y2=bottom, z1=0, z2=0, color=QColor(255, 255, 255, 25))) for number in range(top, bottom + 1000, 1000): self.raw['grid'].append( MapLine(y1=number, y2=number, x1=left, x2=right, z1=0, z2=0, color=QColor(255, 255, 255, 25))) self.grid = QGraphicsPathItem() line_path = QPainterPath() for line in self.raw['grid']: line_path.moveTo(line.x1, line.y1) line_path.lineTo(line.x2, line.y2) self.grid.setPath(line_path) self.grid.setPen( QPen(line.color, config.data['maps']['grid_line_width'])) self.grid.setZValue(0) # Get z levels counter = Counter(all_z) # bunch together zgroups based on peaks with floor being low point before rise z_groups = [] last_value = None first_run = True for z in sorted(counter.items(), key=lambda x: x[0]): if last_value is None: last_value = z continue if (abs(last_value[0] - z[0]) < 20) or z[1] < 8: last_value = (last_value[0], last_value[1] + z[1]) else: if first_run: first_run = False if last_value[1] < 40 or abs(last_value[0] - z[0]) < 18: last_value = z continue z_groups.append(last_value[0]) last_value = z # get last iteration if last_value[1] > 50: z_groups.append(last_value[0]) self._z_groups = z_groups # Create QGraphicsPathItem for lines seperately to retain colors temp_dict = {} for l in self.raw['lines']: lz = min(l.z1, l.z2) lz = self.get_closest_z_group(lz) if not temp_dict.get(lz, None): temp_dict[lz] = {'paths': {}} lc = l.color.getRgb() if not temp_dict[lz]['paths'].get(lc, None): path_item = QGraphicsPathItem() path_item.setPen( QPen(l.color, config.data['maps']['line_width'])) temp_dict[lz]['paths'][lc] = path_item path = temp_dict[lz]['paths'][lc].path() path.moveTo(l.x1, l.y1) path.lineTo(l.x2, l.y2) temp_dict[lz]['paths'][lc].setPath(path) # Group QGraphicsPathItems into QGraphicsItemGroups and update self for z in temp_dict.keys(): item_group = QGraphicsItemGroup() for (_, path) in temp_dict[z]['paths'].items(): item_group.addToGroup(path) self[z] = {'paths': None, 'poi': []} self[z]['paths'] = item_group # Create Points of Interest for p in self.raw['poi']: z = self.get_closest_z_group(p.z) self[z]['poi'].append(PointOfInterest(location=p)) self.geometry = MapGeometry( lowest_x=lowest_x, highest_x=highest_x, lowest_y=lowest_y, highest_y=highest_y, lowest_z=lowest_z, highest_z=highest_z, center_x=int(highest_x - (highest_x - lowest_x) / 2), center_y=int(highest_y - (highest_y - lowest_y) / 2), width=int(highest_x - lowest_x), height=int(highest_y - lowest_y), z_groups=z_groups) def get_closest_z_group(self, z): closest = min(self._z_groups, key=lambda x: abs(x - z)) if z < closest: lower_index = self._z_groups.index(closest) - 1 if lower_index > -1: closest = self._z_groups[lower_index] return closest @staticmethod def get_zone_dict(): # Load Map Pairs from map_keys.ini zone_dict = {} with open(MAP_KEY_FILE, 'r') as file: for line in file.readlines(): values = line.split('=') zone_dict[values[0].strip()] = values[1].strip() return zone_dict @staticmethod def color_transform(color): lightness = color.lightness() if lightness == 0: return QColor(255, 255, 255) elif (color.red == color.green == color.blue): return QColor(255, 255, 255) elif lightness < 150: return color.lighter(150) return color
class CharItem(QGraphicsRectItem): """ This item represents character item The purpose of the class is to draw a character, create a matrix of rectangles and resolve in which rectangles the character passes The class allow the following operations -# Drawing a character using the mouse events: -# Start by the mouse press event -# Continues by the mouse move event -# The character is stored in QGraphicsPathItem -# Transform the character to occupy the whole item's space -# Set operation : resolving the Occupied matrix which tell on which rectangle the character passes -# Reset operation : reverse the character transform so it is possible to continue drawing the character -# Save operation : To a QDataStream -# Load operation : From a QDataStream The graphical view of the class is composed from: -# This class which inherits from QGraphicsRectItem and holds : -# A QGraphicsPathItem : representing the character -# A QGraphicsPathItem : representing the occupied rectangles -# A QGraphicsPathItem : representing the unoccupied rectangles """ def __init__(self, rect: QRectF, pos: QPointF, viewIndex: int=-1): """ CharItem constructor Args: rect (QRectF) : The rectangle that the character should fill pos (QPointF) : The position of the item within the parent viewIndex (int) : The index of the item in case it is presented in multi character presentation """ super(CharItem, self).__init__(rect) self.setAcceptedMouseButtons(Qt.LeftButton) self.setPresentationPrms() self.occupied = [[False for idx in range(self.netCols)] for idx in range(self.netRows)] self.charPath = None self.wasSetted = False self.occupiedPathItem = None self.unoccupiedPathItem = None self.dirty = False self.viewIndex = viewIndex self.filename = "" self.boundaries = rect self.dx = 1 self.dy = 1 self.posInParent = pos self.setPos(self.posInParent) def setPresentationPrms(self): """ Setting the presentation prms The reason the there is a duplicate set of presentation parameters is that it allows changing the presentation parameters for one character (like in the select option """ self.netColor = netColor self.netThickness = netThickness self.occupyColor = occupyColor self.unOccupyColor = unOccupyColor self.shapeColor = shapeColor self.shapeLineThickness = shapeLineThickness self.selectedOccupiedColor = selectedOccupiedColor self.selectedShapeColor = selectedShapeColor self.netRows = netRows self.netCols = netCols def setNetBoxDimensions(self, rect: QRectF): """ Set net box dimensions The net box is the rectangle that compose the network drawn to show the occupy matrix """ self.netRectHeight = rect.height() / self.netRows self.netRectWidth = rect.width() / self.netCols self.left = rect.left() self.top = rect.top() def netRect(self, row_idx: int, col_idx: int) -> QRectF: """ Set net rect The net box is the rectangle that compose the network drawn to show the occupy matrix Args: row_idx (int) : The row of the network rectangle col_idx (int) : The col of the network rectangle Returns: QRectF : The rectangle """ return QRectF(self.left + col_idx * self.netRectWidth, self.top + row_idx * self.netRectHeight, self.netRectWidth, self.netRectHeight) def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent): """ Mouse move event : continue draw a line This event is activated when the mouse is pressed and moves The methods draws the line in 2 conditions: -# The item is not part of multi character presentation -# A character path was initiated (using the mouse press event) Args: event (QGraphicsSceneMouseEvent) : the event description """ if self.viewIndex == -1: if self.charPath is not None: point = event.scenePos() path = self.charPath.path() path.lineTo(point) self.charPath.setPath(path) self.update() def mousePressEvent(self, event: QGraphicsSceneMouseEvent): """ Mouse Press Event : Start a new line / Select the character If the character is part of multi character presentation activate the character selection If the character is in single character presentation Start a new line in the character Args: event (QGraphicsSceneMouseEvent) : the event description """ if self.viewIndex == -1: self.startLine(event) else: self.setSelected() def startLine(self, event: QGraphicsSceneMouseEvent): """ Start drawing a line When the mouse button is pressed and we are in single character dialog this method is activated to start drowning a line in the character Args: event (QGraphicsSceneMouseEvent) : the event description """ # There are 2 modes for the presentation: # Original mode where the character is it's original size # After setting mode when the set was done and the character fullfill all # the item's space # Drawing can be done only in original mode if self.wasSetted: QMessageBox.critical(None, "Char identifier window", "The shape was already setted use revert setting") return # If this is the first start of a line - generate the QPainterPath and QGraphicsPathItem if self.charPath is None: self.initCharPath() # Move to the mouse position point = event.scenePos() path = self.charPath.path() path.moveTo(point) self.charPath.setPath(path) self.dirty = True def initCharPath(self): """ Init the item that holds the character There is one path item that holds the character This method is activated by start line if the char item was not created to create the new and only one """ self.dirty = True self.charPath = QGraphicsPathItem(self) self.charPath.setPen(QPen(QColor(self.shapeColor), self.shapeLineThickness)) self.charPath.setZValue(1) self.charPath.originalPos = self.charPath.pos() self.charPath.setPath(QPainterPath()) def setSelected(self): """ Set the item a selected item This method is activated when the mouse button is presses and the item is part of multi character presentation """ # Set the colors of the item self.occupiedPathItem.setBrush(QBrush(QColor(self.selectedOccupiedColor))) self.charPath.setPen(QPen(QColor(self.selectedShapeColor), self.shapeLineThickness)) self.update() # Report to the parent item about the selection self.parentItem().setSelected(self.viewIndex) def resetSelected(self): """ Set the colors of the item to not selected """ self.occupiedPathItem.setBrush(QBrush(QColor(self.occupyColor))) self.charPath.setPen(QPen(QColor(self.shapeColor), self.shapeLineThickness)) self.update() def set(self): """ Calculate the occupied matrix and present the results This method does the following: -# Fill the occupied matrix -# Generate the occupied and unoccupied pathes items -# Transform the char path to fit to the item's boundaries """ # If there is no shape drawn - return if self.charPath is None: QMessageBox.critical(None, "Char identifier window", "There is no shape drawn") return # If the item is in setted mode - return if self.wasSetted: QMessageBox.critical(None, "Char identifier window", "The shape was already setted use revert setting") return # fill the occupied matrix with the data before the scaling self.fillOccupied() self.setNetBoxDimensions(self.boundingRect()) self.createNetPaths() # update the transform - change the dimensions and location # only on the first time self.transformCharPath() self.wasSetted = True # update the presentation self.update() def revertTransform(self): """ Change from Setted mode to drawing mode The drawing mode is the mode where the character can be drawn -# Restore the original size of the character (Reset the transform of the char item) -# Restor the char path item's position to the original one (saved when created) -# Empty the occupiedPath and the unoccupiedPath """ # If there is no character drawn - return if self.charPath is None: QMessageBox.critical(None, "Char identifier window", "There is no shape drawn") return # If the item is already in drawing mode - return if not self.wasSetted: QMessageBox.critical(None, "Char identifier window", "The shape was not setted use set button") return # The char path item transform = self.charPath.transform() transform.reset() # The self.dx and self.dy are the scale parameters created when the item # begins and they are the scale parameters that transform it to the boundaries # given by the parent item transform.scale(self.dx, self.dy) self.charPath.setTransform(transform) self.charPath.setPos(self.charPath.originalPos) # Empty the network pathes self.occupiedPathItem.setPath(QPainterPath()) self.unoccupiedPathItem.setPath(QPainterPath()) self.wasSetted = False def transformCharPath(self): """ Transform char path when the item is setted This method does the following -# scale the char path to the size of the item -# calculate the new position of the char path so that it will be placed at the top left corner of the item """ dx = self.boundingRect().width() / self.charPath.boundingRect().width() dy = self.boundingRect().height() / self.charPath.boundingRect().height() transform = self.charPath.transform() transform.reset() transform.scale(dx, dy) self.charPath.setTransform(transform) # Move the shape to the origin moveX = -(self.charPath.boundingRect().left() - self.boundingRect().left()) * dx moveY = -(self.charPath.boundingRect().top() - self.boundingRect().top()) * dy self.charPath.setX(self.charPath.x() + moveX) self.charPath.setY(self.charPath.y() + moveY) def fillOccupied(self): """ Fill the occupied matrix The algorithm of filling the occupied matrix is -# Scanning the char path -# For each point decide on where row and column of the net -# Set the occupies matrix for this column and row to True """ for idx in range(100): point = self.charPath.path().pointAtPercent(idx / 100.) row_idx, col_idx = self.calcRowCol(point) self.occupied[row_idx][col_idx] = True def calcRowCol(self, point: QPointF): """ Calculate the network row and column that a point is int calc the row and column indexes of a point The following is the algorithm: 1. Find the distance between the point and the left (or top) 2. Divide the distance with the width of path to find the relative position 3. Multipile this relative position with the number of rows/cols 4. Convert the result to int to find the indexes 5. If the index is the number of row/col reduce the index (This is for the case the the point is on the boundary and in this case the relative position is 1 which will cause the indexes to be the number of rows/cols - out of the matrix indexes) Args: point (QPointF) : The point to resolve Returns: int : The network row that the point is in int : The network column that the point is in """ partialX = (point.x() - self.charPath.boundingRect().left()) / self.charPath.boundingRect().width() partialY = (point.y() - self.charPath.boundingRect().top()) / self.charPath.boundingRect().height() col_idx = int(partialX * self.netCols) row_idx = int(partialY * self.netRows) if row_idx == self.netRows: row_idx -= 1 if col_idx == self.netCols: col_idx -= 1 return row_idx, col_idx def createNetPaths(self): """ Create the network pathes This method creates 2 network pathes items one for holding the occupied rectangles and one to hold the unoccupied rectangles """ # Generate 2 QPainterPath occupiedPath = QPainterPath() unoccupiedPath = QPainterPath() # For each entry in occupied matrix : # Add a rectangle to the appropriate path according the entry value for row_idx in range(self.netRows): for col_idx in range(self.netCols): if self.occupied[row_idx][col_idx]: occupiedPath.addRect(self.netRect(row_idx, col_idx)) else: unoccupiedPath.addRect(self.netRect(row_idx, col_idx)) # Create the QGraphicsPathItems that will hold the path self.createNetPath(self.occupyColor, occupiedPath, True) self.createNetPath(self.unOccupyColor, unoccupiedPath, False) def createNetPath(self, brushColor: str, painterPath: QPainterPath, isOccupyPathItem: bool): """ Create a QGraphicsPathItem for a network path Args: brushColor (str) : The color for filling the rectangles painterPath (QPainterPath) : The path to be inserted to the item isOccupyPathItem (bool) : Whether the path is occupied or unoccupied path """ # Generate the path item if not created if isOccupyPathItem: if self.occupiedPathItem is None: self.occupiedPathItem = QGraphicsPathItem(self) pathItem = self.occupiedPathItem else: if self.unoccupiedPathItem is None: self.unoccupiedPathItem = QGraphicsPathItem(self) pathItem = self.unoccupiedPathItem if pathItem is None: pathItem = QGraphicsPathItem(self) # Set the item parameters pathItem.setPath(painterPath) pathItem.setPen(QPen(QColor(self.netColor), self.netThickness, style=Qt.SolidLine)) pathItem.setBrush(QBrush(QColor(brushColor))) pathItem.setZValue(0) def save(self, stream: QDataStream, filename: str): """ Save the item to QDataStream Args: stream (QDataStream) : The data stream to write the item to filename (str) : The filename (for documenting purposes) """ # The item position stream << self.pos() # The dimensions stream << self.rect() # The presentation parameters stream.writeQString(self.netColor) stream.writeQString(self.occupyColor) stream.writeQString(self.unOccupyColor) stream.writeQString(self.shapeColor) stream.writeInt16(self.shapeLineThickness) stream.writeInt16(self.netRows) stream.writeInt16(self.netRows) # The items paths stream << self.charPath.path() self.dirty = False self.filename = filename def load(self, stream, filename): """ Loads the item from QDataStream Args: stream (QDataStream) : The data stream to read the item from filename (str) : The filename (for documenting purposes) """ # read the pos pos = QPointF() stream >> pos self.setPos(pos) # read the dimensions rect = QRectF() stream >> rect self.setRect(rect) # The presentation parameters self.netColor = stream.readQString() self.occupyColor = stream.readQString() self.unOccupyColor = stream.readQString() self.shapeColor = stream.readQString() self.shapeLineThickness = stream.readInt16() self.netRows = stream.readInt16() self.netRows = stream.readInt16() # read the paths self.initCharPath() path = self.charPath.path() stream >> path self.charPath.setPath(path) # Fit the item to the boundaries and position given by the item's parent self.fitToBoundaries() # The presentation of the item is in setted mode so we activate the set method self.wasSetted = False self.set() self.dirty = False self.filename = filename def fitToBoundaries(self): """ Fit the item to the boundaries and position given by it's parent This method was made to support the change of the character boundaries and that the char can be presented in different boundaries and position """ self.setPos(self.posInParent) self.dx = self.boundaries.width() / self.rect().width() self.dy = self.boundaries.height() / self.rect().height() transform = self.transform() transform.scale(self.dx, self.dy) self.setTransform(transform)
class NodeItem(QGraphicsObject): """ An widget node item in the canvas. """ #: Signal emitted when the scene position of the node has changed. positionChanged = Signal() #: Signal emitted when the geometry of the channel anchors changes. anchorGeometryChanged = Signal() #: Signal emitted when the item has been activated (by a mouse double #: click or a keyboard) activated = Signal() #: The item is under the mouse. hovered = Signal() #: Span of the anchor in degrees ANCHOR_SPAN_ANGLE = 90 #: Z value of the item Z_VALUE = 100 def __init__(self, widget_description=None, parent=None, **kwargs): self.__boundingRect = None QGraphicsObject.__init__(self, parent, **kwargs) self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True) self.setFlag(QGraphicsItem.ItemHasNoContents, True) self.setFlag(QGraphicsItem.ItemIsSelectable, True) self.setFlag(QGraphicsItem.ItemIsMovable, True) self.setFlag(QGraphicsItem.ItemIsFocusable, True) # central body shape item self.shapeItem = None # in/output anchor items self.inputAnchorItem = None self.outputAnchorItem = None # title text item self.captionTextItem = None # error, warning, info items self.errorItem = None self.warningItem = None self.infoItem = None # background when selected self.backgroundItem = None self.__title = "" self.__processingState = 0 self.__progress = -1 self.__statusMessage = "" self.__error = None self.__warning = None self.__info = None self.__anchorLayout = None self.__animationEnabled = False self.setZValue(self.Z_VALUE) self.setupGraphics() self.setWidgetDescription(widget_description) @classmethod def from_node(cls, node): """ Create an :class:`NodeItem` instance and initialize it from a :class:`SchemeNode` instance. """ self = cls() self.setWidgetDescription(node.description) # self.setCategoryDescription(node.category) return self @classmethod def from_node_meta(cls, meta_description): """ Create an `NodeItem` instance from a node meta description. """ self = cls() self.setWidgetDescription(meta_description) return self def setupGraphics(self): """ Set up the graphics. """ shape_rect = QRectF(-24, -24, 48, 48) self.shapeItem = NodeBodyItem(self) self.shapeItem.setShapeRect(shape_rect) self.shapeItem.setAnimationEnabled(self.__animationEnabled) # Rect for widget's 'ears'. anchor_rect = QRectF(-31, -31, 62, 62) self.inputAnchorItem = SinkAnchorItem(self) input_path = QPainterPath() start_angle = 180 - self.ANCHOR_SPAN_ANGLE / 2 input_path.arcMoveTo(anchor_rect, start_angle) input_path.arcTo(anchor_rect, start_angle, self.ANCHOR_SPAN_ANGLE) self.inputAnchorItem.setAnchorPath(input_path) self.outputAnchorItem = SourceAnchorItem(self) output_path = QPainterPath() start_angle = self.ANCHOR_SPAN_ANGLE / 2 output_path.arcMoveTo(anchor_rect, start_angle) output_path.arcTo(anchor_rect, start_angle, - self.ANCHOR_SPAN_ANGLE) self.outputAnchorItem.setAnchorPath(output_path) self.inputAnchorItem.hide() self.outputAnchorItem.hide() # Title caption item self.captionTextItem = NameTextItem(self) self.captionTextItem.setPlainText("") self.captionTextItem.setPos(0, 33) def iconItem(standard_pixmap): item = GraphicsIconItem(self, icon=standard_icon(standard_pixmap), iconSize=QSize(16, 16)) item.hide() return item self.errorItem = iconItem(QStyle.SP_MessageBoxCritical) self.warningItem = iconItem(QStyle.SP_MessageBoxWarning) self.infoItem = iconItem(QStyle.SP_MessageBoxInformation) ################################ # PyQt 5.10 crashes because of this call (2) #self.backgroundItem = QGraphicsPathItem(self) self.backgroundItem = QGraphicsPathItem(None) ################################ backgroundrect = QPainterPath() backgroundrect.addRoundedRect(anchor_rect.adjusted(-4, -2, 4, 2), 5, 5, mode=Qt.AbsoluteSize) self.backgroundItem.setPen(QPen(Qt.NoPen)) self.backgroundItem.setBrush(QPalette().brush(QPalette.Highlight)) self.backgroundItem.setOpacity(0.5) self.backgroundItem.setPath(backgroundrect) self.backgroundItem.setZValue(-10) self.backgroundItem.setVisible(self.isSelected()) self.prepareGeometryChange() self.__boundingRect = None # TODO: Remove the set[Widget|Category]Description. The user should # handle setting of icons, title, ... def setWidgetDescription(self, desc): """ Set widget description. """ self.widget_description = desc if desc is None: return icon = icon_loader.from_description(desc).get(desc.icon) if icon: self.setIcon(icon) if not self.title(): self.setTitle(desc.name) if desc.inputs: self.inputAnchorItem.show() if desc.outputs: self.outputAnchorItem.show() tooltip = NodeItem_toolTipHelper(self) self.setToolTip(tooltip) def setWidgetCategory(self, desc): """ Set the widget category. """ self.category_description = desc if desc and desc.background: background = NAMED_COLORS.get(desc.background, desc.background) color = QColor(background) if color.isValid(): self.setColor(color) def setIcon(self, icon): """ Set the node item's icon (:class:`QIcon`). """ if isinstance(icon, QIcon): self.icon_item = GraphicsIconItem(self.shapeItem, icon=icon, iconSize=QSize(36, 36)) self.icon_item.setPos(-18, -18) else: raise TypeError def setColor(self, color, selectedColor=None): """ Set the widget color. """ if selectedColor is None: selectedColor = saturated(color, 150) palette = create_palette(color, selectedColor) self.shapeItem.setPalette(palette) def setPalette(self, palette): # TODO: The palette should override the `setColor` raise NotImplementedError def setTitle(self, title): """ Set the node title. The title text is displayed at the bottom of the node. """ self.__title = title self.__updateTitleText() def title(self): """ Return the node title. """ return self.__title title_ = Property(six.text_type, fget=title, fset=setTitle, doc="Node title text.") def setFont(self, font): """ Set the title text font (:class:`QFont`). """ if font != self.font(): self.prepareGeometryChange() self.captionTextItem.setFont(font) self.__updateTitleText() def font(self): """ Return the title text font. """ return self.captionTextItem.font() def setAnimationEnabled(self, enabled): """ Set the node animation enabled state. """ if self.__animationEnabled != enabled: self.__animationEnabled = enabled self.shapeItem.setAnimationEnabled(enabled) def animationEnabled(self): """ Are node animations enabled. """ return self.__animationEnabled def setProcessingState(self, state): """ Set the node processing state i.e. the node is processing (is busy) or is idle. """ if self.__processingState != state: self.__processingState = state self.shapeItem.setProcessingState(state) if not state: # Clear the progress meter. self.setProgress(-1) if self.__animationEnabled: self.shapeItem.ping() def processingState(self): """ The node processing state. """ return self.__processingState processingState_ = Property(int, fget=processingState, fset=setProcessingState) def setProgress(self, progress): """ Set the node work progress state (number between 0 and 100). """ if progress is None or progress < 0 or not self.__processingState: progress = -1 progress = max(min(progress, 100), -1) if self.__progress != progress: self.__progress = progress self.shapeItem.setProgress(progress) self.__updateTitleText() def progress(self): """ Return the node work progress state. """ return self.__progress progress_ = Property(float, fget=progress, fset=setProgress, doc="Node progress state.") def setStatusMessage(self, message): """ Set the node status message text. This text is displayed below the node's title. """ if self.__statusMessage != message: self.__statusMessage = six.text_type(message) self.__updateTitleText() def statusMessage(self): return self.__statusMessage def setStateMessage(self, message): """ Set a state message to display over the item. Parameters ---------- message : UserMessage Message to display. `message.severity` is used to determine the icon and `message.contents` is used as a tool tip. """ # TODO: Group messages by message_id not by severity # and deprecate set[Error|Warning|Error]Message if message.severity == UserMessage.Info: self.setInfoMessage(message.contents) elif message.severity == UserMessage.Warning: self.setWarningMessage(message.contents) elif message.severity == UserMessage.Error: self.setErrorMessage(message.contents) def setErrorMessage(self, message): if self.__error != message: self.__error = message self.__updateMessages() def setWarningMessage(self, message): if self.__warning != message: self.__warning = message self.__updateMessages() def setInfoMessage(self, message): if self.__info != message: self.__info = message self.__updateMessages() def newInputAnchor(self): """ Create and return a new input :class:`AnchorPoint`. """ if not (self.widget_description and self.widget_description.inputs): raise ValueError("Widget has no inputs.") anchor = AnchorPoint() self.inputAnchorItem.addAnchor(anchor, position=1.0) positions = self.inputAnchorItem.anchorPositions() positions = uniform_linear_layout(positions) self.inputAnchorItem.setAnchorPositions(positions) return anchor def removeInputAnchor(self, anchor): """ Remove input anchor. """ self.inputAnchorItem.removeAnchor(anchor) positions = self.inputAnchorItem.anchorPositions() positions = uniform_linear_layout(positions) self.inputAnchorItem.setAnchorPositions(positions) def newOutputAnchor(self): """ Create and return a new output :class:`AnchorPoint`. """ if not (self.widget_description and self.widget_description.outputs): raise ValueError("Widget has no outputs.") anchor = AnchorPoint(self) self.outputAnchorItem.addAnchor(anchor, position=1.0) positions = self.outputAnchorItem.anchorPositions() positions = uniform_linear_layout(positions) self.outputAnchorItem.setAnchorPositions(positions) return anchor def removeOutputAnchor(self, anchor): """ Remove output anchor. """ self.outputAnchorItem.removeAnchor(anchor) positions = self.outputAnchorItem.anchorPositions() positions = uniform_linear_layout(positions) self.outputAnchorItem.setAnchorPositions(positions) def inputAnchors(self): """ Return a list of all input anchor points. """ return self.inputAnchorItem.anchorPoints() def outputAnchors(self): """ Return a list of all output anchor points. """ return self.outputAnchorItem.anchorPoints() def setAnchorRotation(self, angle): """ Set the anchor rotation. """ self.inputAnchorItem.setRotation(angle) self.outputAnchorItem.setRotation(angle) self.anchorGeometryChanged.emit() def anchorRotation(self): """ Return the anchor rotation. """ return self.inputAnchorItem.rotation() def boundingRect(self): # TODO: Important because of this any time the child # items change geometry the self.prepareGeometryChange() # needs to be called. if self.__boundingRect is None: self.__boundingRect = self.childrenBoundingRect() return self.__boundingRect def shape(self): # Shape for mouse hit detection. # TODO: Should this return the union of all child items? return self.shapeItem.shape() def __updateTitleText(self): """ Update the title text item. """ text = ['<div align="center">%s' % escape(self.title())] status_text = [] progress_included = False if self.__statusMessage: msg = escape(self.__statusMessage) format_fields = dict(parse_format_fields(msg)) if "progress" in format_fields and len(format_fields) == 1: # Insert progress into the status text format string. spec, _ = format_fields["progress"] if spec != None: progress_included = True progress_str = "{0:.0f}%".format(self.progress()) status_text.append(msg.format(progress=progress_str)) else: status_text.append(msg) if self.progress() >= 0 and not progress_included: status_text.append("%i%%" % int(self.progress())) if status_text: text += ["<br/>", '<span style="font-style: italic">', "<br/>".join(status_text), "</span>"] text += ["</div>"] text = "".join(text) # The NodeItems boundingRect could change. self.prepareGeometryChange() self.__boundingRect = None self.captionTextItem.setHtml(text) self.captionTextItem.document().adjustSize() width = self.captionTextItem.textWidth() self.captionTextItem.setPos(-width / 2.0, 33) def __updateMessages(self): """ Update message items (position, visibility and tool tips). """ items = [self.errorItem, self.warningItem, self.infoItem] messages = [self.__error, self.__warning, self.__info] for message, item in zip(messages, items): item.setVisible(bool(message)) item.setToolTip(message or "") shown = [item for item in items if item.isVisible()] count = len(shown) if count: spacing = 3 rects = [item.boundingRect() for item in shown] width = sum(rect.width() for rect in rects) width += spacing * max(0, count - 1) height = max(rect.height() for rect in rects) origin = self.shapeItem.boundingRect().top() - spacing - height origin = QPointF(-width / 2, origin) for item, rect in zip(shown, rects): item.setPos(origin) origin = origin + QPointF(rect.width() + spacing, 0) def mousePressEvent(self, event): if self.shapeItem.path().contains(event.pos()): return QGraphicsObject.mousePressEvent(self, event) else: event.ignore() def mouseDoubleClickEvent(self, event): if self.shapeItem.path().contains(event.pos()): QGraphicsObject.mouseDoubleClickEvent(self, event) QTimer.singleShot(0, self.activated.emit) else: event.ignore() def contextMenuEvent(self, event): if self.shapeItem.path().contains(event.pos()): return QGraphicsObject.contextMenuEvent(self, event) else: event.ignore() def focusInEvent(self, event): self.shapeItem.setHasFocus(True) return QGraphicsObject.focusInEvent(self, event) def focusOutEvent(self, event): self.shapeItem.setHasFocus(False) return QGraphicsObject.focusOutEvent(self, event) def itemChange(self, change, value): if change == QGraphicsItem.ItemSelectedChange: selected = bool(qtcompat.qunwrap(value)) self.shapeItem.setSelected(selected) self.captionTextItem.setSelectionState(selected) self.backgroundItem.setVisible(selected) elif change == QGraphicsItem.ItemPositionHasChanged: self.positionChanged.emit() return QGraphicsObject.itemChange(self, change, value)